feat(ollama): OllamaSettingsModal + App mount + OllamaBanner 설정 링크 (v0.2.3.1)
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
<main className="main">
|
||||
{!showTrash && (
|
||||
<>
|
||||
<OllamaBanner />
|
||||
<OllamaBanner onOpenSettings={() => setOllamaSettingsOpen(true)} />
|
||||
<RecoveryToast
|
||||
show={showRecovery}
|
||||
onDismiss={() => { markRecoveryDismissed(); setRecoveryDismissed(true); }}
|
||||
@@ -155,6 +158,10 @@ export function App(): React.ReactElement {
|
||||
)}
|
||||
</main>
|
||||
<TagUndoToast />
|
||||
<OllamaSettingsModal
|
||||
open={ollamaSettingsOpen}
|
||||
onClose={() => setOllamaSettingsOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
>
|
||||
재확인
|
||||
</button>
|
||||
{onOpenSettings && (
|
||||
<button
|
||||
onClick={onOpenSettings}
|
||||
style={{
|
||||
background: 'transparent', color: 'inherit',
|
||||
border: '1px solid currentColor', borderRadius: 4,
|
||||
padding: '2px 8px', fontSize: 12, cursor: 'pointer',
|
||||
marginLeft: 6
|
||||
}}
|
||||
>
|
||||
설정
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{status.reason ? (
|
||||
<span style={{ fontSize: 11, opacity: 0.7, marginTop: 4 }}>
|
||||
|
||||
117
src/renderer/inbox/components/OllamaSettingsModal.tsx
Normal file
117
src/renderer/inbox/components/OllamaSettingsModal.tsx
Normal file
@@ -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<string | null>(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 (
|
||||
<div style={{
|
||||
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#fff', borderRadius: 8, padding: 20, minWidth: 400, maxWidth: 500,
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.2)'
|
||||
}}>
|
||||
<h2 style={{ margin: '0 0 12px 0', fontSize: 16 }}>Ollama 설정</h2>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 4 }}>
|
||||
Endpoint URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={endpoint}
|
||||
onChange={(e) => setEndpoint(e.target.value)}
|
||||
placeholder="http://localhost:11434"
|
||||
style={{ width: '100%', padding: '6px 8px', fontSize: 13, border: '1px solid #ccc', borderRadius: 4 }}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 4 }}>
|
||||
Model
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={model}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
placeholder="gemma4:e4b"
|
||||
style={{ width: '100%', padding: '6px 8px', fontSize: 13, border: '1px solid #ccc', borderRadius: 4 }}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<div style={{
|
||||
background: '#fce4e4', color: '#a33', padding: '6px 10px', borderRadius: 4,
|
||||
fontSize: 12, marginBottom: 12
|
||||
}}>
|
||||
저장 실패: {error}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={saving}
|
||||
style={{
|
||||
background: 'transparent', color: '#666',
|
||||
border: '1px solid #ccc', borderRadius: 4,
|
||||
padding: '6px 14px', fontSize: 12, cursor: saving ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
onClick={() => void handleSave()}
|
||||
disabled={saving}
|
||||
style={{
|
||||
background: saving ? '#999' : '#0a4b80', color: '#fff',
|
||||
border: 'none', borderRadius: 4,
|
||||
padding: '6px 14px', fontSize: 12, cursor: saving ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
{saving ? '검증 중...' : '저장'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -91,6 +91,7 @@ export interface InboxApi {
|
||||
emitRecallSnoozed(id: string): Promise<void>;
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user