From 833a59836817b5da9a657671ce70713ba3100ebf Mon Sep 17 00:00:00 2001 From: altair823 Date: Mon, 4 May 2026 23:40:31 +0900 Subject: [PATCH] =?UTF-8?q?feat(ollama):=20OllamaSettingsModal=20+=20App?= =?UTF-8?q?=20mount=20+=20OllamaBanner=20=EC=84=A4=EC=A0=95=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20(v0.2.3.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OllamaSettingsModal: endpoint + model freetext 입력, 저장 시 healthCheck → 성공 닫기, 실패 inline 에러 - App.tsx: ollamaSettingsOpen state + onOpenOllamaSettings IPC subscribe - OllamaBanner: onOpenSettings prop 추가, 우측 "설정" 버튼 - preload + types: onOpenOllamaSettings listener bridge Co-Authored-By: Claude Opus 4.7 (1M context) --- src/preload/index.ts | 7 +- src/renderer/inbox/App.tsx | 11 +- .../inbox/components/OllamaBanner.tsx | 19 ++- .../inbox/components/OllamaSettingsModal.tsx | 117 ++++++++++++++++++ src/shared/types.ts | 1 + 5 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 src/renderer/inbox/components/OllamaSettingsModal.tsx diff --git a/src/preload/index.ts b/src/preload/index.ts index b81e134..1ba817c 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -46,7 +46,12 @@ const api: InklingApi = { emitRecallShown: (id: string) => ipcRenderer.invoke('inbox:emitRecallShown', id), emitRecallSnoozed: (id: string) => ipcRenderer.invoke('inbox:emitRecallSnoozed', id), loadOllamaSettings: () => ipcRenderer.invoke('inbox:loadOllamaSettings'), - saveOllamaSettings: (v: { endpoint: string; model: string }) => ipcRenderer.invoke('inbox:saveOllamaSettings', v) + saveOllamaSettings: (v: { endpoint: string; model: string }) => ipcRenderer.invoke('inbox:saveOllamaSettings', v), + onOpenOllamaSettings: (cb: () => void) => { + const handler = () => cb(); + ipcRenderer.on('inbox:openOllamaSettings', handler); + return () => ipcRenderer.removeListener('inbox:openOllamaSettings', handler); + }, } }; diff --git a/src/renderer/inbox/App.tsx b/src/renderer/inbox/App.tsx index b9b8719..0926b68 100644 --- a/src/renderer/inbox/App.tsx +++ b/src/renderer/inbox/App.tsx @@ -12,6 +12,7 @@ import { TagUndoToast } from './components/TagUndoToast.js'; import { ExpiryBanner } from './components/ExpiryBanner.js'; import { FailedBanner } from './components/FailedBanner.js'; import { RecallBanner } from './components/RecallBanner.js'; +import { OllamaSettingsModal } from './components/OllamaSettingsModal.js'; export function App(): React.ReactElement { const { @@ -21,6 +22,7 @@ export function App(): React.ReactElement { toggleShowTrash, restoreNote, permanentDeleteNote, emptyTrash } = useInbox(); const [recoveryDismissed, setRecoveryDismissed] = useState(isRecoveryDismissedToday()); + const [ollamaSettingsOpen, setOllamaSettingsOpen] = useState(false); useEffect(() => { void loadInitial(); @@ -31,9 +33,10 @@ export function App(): React.ReactElement { const unsubOllama = inboxApi.onOllamaStatus((status) => { useInbox.setState({ ollamaStatus: status }); }); + const unsubOllamaSettings = inboxApi.onOpenOllamaSettings(() => setOllamaSettingsOpen(true)); const onFocus = () => { void refreshMeta(); }; window.addEventListener('focus', onFocus); - return () => { unsubNote(); unsubOllama(); window.removeEventListener('focus', onFocus); }; + return () => { unsubNote(); unsubOllama(); unsubOllamaSettings(); window.removeEventListener('focus', onFocus); }; // onOllamaStatus 콜백은 useInbox.setState 직접 호출 — store reference 가 안정적이라 // deps array 에 추가 불필요. mount 시 1회 구독 + unmount 시 해제. }, [loadInitial, refreshMeta, upsertNote]); @@ -79,7 +82,7 @@ export function App(): React.ReactElement {
{!showTrash && ( <> - + setOllamaSettingsOpen(true)} /> { markRecoveryDismissed(); setRecoveryDismissed(true); }} @@ -155,6 +158,10 @@ export function App(): React.ReactElement { )}
+ setOllamaSettingsOpen(false)} + /> ); } diff --git a/src/renderer/inbox/components/OllamaBanner.tsx b/src/renderer/inbox/components/OllamaBanner.tsx index 0ecfd6d..2de6fa8 100644 --- a/src/renderer/inbox/components/OllamaBanner.tsx +++ b/src/renderer/inbox/components/OllamaBanner.tsx @@ -1,7 +1,11 @@ import React from 'react'; import { useInbox } from '../store.js'; -export function OllamaBanner(): React.ReactElement | null { +interface OllamaBannerProps { + onOpenSettings?: () => void; +} + +export function OllamaBanner({ onOpenSettings }: OllamaBannerProps = {}): React.ReactElement | null { const status = useInbox((s) => s.ollamaStatus); const recheckOllama = useInbox((s) => s.recheckOllama); if (status.ok) return null; @@ -28,6 +32,19 @@ export function OllamaBanner(): React.ReactElement | null { > 재확인 + {onOpenSettings && ( + + )} {status.reason ? ( diff --git a/src/renderer/inbox/components/OllamaSettingsModal.tsx b/src/renderer/inbox/components/OllamaSettingsModal.tsx new file mode 100644 index 0000000..ac39122 --- /dev/null +++ b/src/renderer/inbox/components/OllamaSettingsModal.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useState } from 'react'; +import { inboxApi } from '../api.js'; + +interface Props { + open: boolean; + onClose: () => void; +} + +export function OllamaSettingsModal({ open, onClose }: Props): React.ReactElement | null { + const [endpoint, setEndpoint] = useState('http://localhost:11434'); + const [model, setModel] = useState('gemma4:e4b'); + const [error, setError] = useState(null); + const [saving, setSaving] = useState(false); + + // 마운트/open 시 현재 설정 fetch + useEffect(() => { + if (!open) return; + void inboxApi.loadOllamaSettings().then((s) => { + if (s) { + setEndpoint(s.endpoint); + setModel(s.model); + } + setError(null); + }); + }, [open]); + + if (!open) return null; + + async function handleSave() { + setSaving(true); + setError(null); + try { + const r = await inboxApi.saveOllamaSettings({ endpoint, model }); + if (r.ok) { + onClose(); + } else { + setError(r.reason); + } + } catch (e) { + setError(String(e)); + } finally { + setSaving(false); + } + } + + return ( +
+
+

Ollama 설정

+
+ + setEndpoint(e.target.value)} + placeholder="http://localhost:11434" + style={{ width: '100%', padding: '6px 8px', fontSize: 13, border: '1px solid #ccc', borderRadius: 4 }} + disabled={saving} + /> +
+
+ + setModel(e.target.value)} + placeholder="gemma4:e4b" + style={{ width: '100%', padding: '6px 8px', fontSize: 13, border: '1px solid #ccc', borderRadius: 4 }} + disabled={saving} + /> +
+ {error && ( +
+ 저장 실패: {error} +
+ )} +
+ + +
+
+
+ ); +} diff --git a/src/shared/types.ts b/src/shared/types.ts index 1ff4146..dd79b2c 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -91,6 +91,7 @@ export interface InboxApi { emitRecallSnoozed(id: string): Promise; loadOllamaSettings(): Promise<{ endpoint: string; model: string } | null>; saveOllamaSettings(v: { endpoint: string; model: string }): Promise<{ ok: true } | { ok: false; reason: string }>; + onOpenOllamaSettings(cb: () => void): () => void; } export interface InklingApi {