200 lines
6.2 KiB
TypeScript
200 lines
6.2 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { z } from 'zod';
|
|
import { inboxApi } from '../../api.js';
|
|
import { VisionSection } from './VisionSection.js';
|
|
|
|
const endpointSchema = z.string().url();
|
|
|
|
export function AiProviderSection(): React.ReactElement {
|
|
const [endpoint, setEndpoint] = useState('');
|
|
const [model, setModel] = useState('');
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [saveResult, setSaveResult] = useState<string | null>(null);
|
|
const [recheckResult, setRecheckResult] = useState<string | null>(null);
|
|
// v0.2.9 Cut B Task 15-16: AI 자동 처리 토글 + disabled 메모 일괄 처리.
|
|
const [aiEnabled, setAiEnabledState] = useState<boolean | null>(null);
|
|
const [disabledCount, setDisabledCount] = useState(0);
|
|
|
|
useEffect(() => {
|
|
void (async () => {
|
|
const s = await inboxApi.loadOllamaSettings();
|
|
if (s) {
|
|
setEndpoint(s.endpoint);
|
|
setModel(s.model);
|
|
}
|
|
const settings = await inboxApi.getSettings();
|
|
const enabled = settings.ai_enabled ?? true;
|
|
setAiEnabledState(enabled);
|
|
if (enabled) {
|
|
const c = await inboxApi.getDisabledCount();
|
|
setDisabledCount(c);
|
|
}
|
|
})();
|
|
}, []);
|
|
|
|
async function onToggleAi(checked: boolean): Promise<void> {
|
|
await inboxApi.setAiEnabled(checked);
|
|
setAiEnabledState(checked);
|
|
if (checked) {
|
|
const c = await inboxApi.getDisabledCount();
|
|
setDisabledCount(c);
|
|
} else {
|
|
setDisabledCount(0);
|
|
}
|
|
}
|
|
|
|
async function onProcessDisabled(): Promise<void> {
|
|
await inboxApi.enqueueDisabled();
|
|
setDisabledCount(0);
|
|
}
|
|
|
|
async function onSave(): Promise<void> {
|
|
const r = endpointSchema.safeParse(endpoint);
|
|
if (!r.success) {
|
|
setError('올바른 URL 형식이 아닙니다 (예: http://localhost:11434)');
|
|
setSaveResult(null);
|
|
return;
|
|
}
|
|
if (model.trim() === '') {
|
|
setError('모델 이름을 입력해주세요');
|
|
setSaveResult(null);
|
|
return;
|
|
}
|
|
setError(null);
|
|
const result = await inboxApi.saveOllamaSettings({ endpoint, model });
|
|
if (result.ok) {
|
|
setSaveResult('저장됨');
|
|
} else {
|
|
setSaveResult(null);
|
|
setError(`저장 실패: ${result.reason}`);
|
|
}
|
|
}
|
|
|
|
async function onRecheck(): Promise<void> {
|
|
setRecheckResult('확인 중...');
|
|
const r = await inboxApi.ollamaRecheck();
|
|
setRecheckResult(r.ok ? '연결됨' : `연결 실패: ${r.reason ?? '알 수 없는 이유'}`);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{/* v0.2.9 Cut B Task 15 — AI 자동 처리 토글 (가장 위, 스위치 의미가 가장 큰 결정) */}
|
|
{aiEnabled !== null && (
|
|
<label style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 12, fontSize: 13 }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={aiEnabled}
|
|
onChange={(e) => void onToggleAi(e.target.checked)}
|
|
/>
|
|
AI 자동 처리 사용
|
|
</label>
|
|
)}
|
|
{aiEnabled === false && (
|
|
<p style={{ fontSize: 12, color: '#666', marginBottom: 12 }}>
|
|
원문만 저장 모드. 메모의 제목/요약/태그가 자동 생성되지 않습니다.<br />
|
|
<a href="https://ollama.com/download" target="_blank" rel="noopener noreferrer">
|
|
Ollama 설치 가이드
|
|
</a>
|
|
</p>
|
|
)}
|
|
{/* v0.2.9 Cut B Task 16 — ON 전환 후 disabled 메모 일괄 처리 prompt */}
|
|
{aiEnabled === true && disabledCount > 0 && (
|
|
<div style={{ padding: 8, background: '#fffbe5', borderRadius: 4, marginBottom: 12, fontSize: 13 }}>
|
|
원문만 저장된 메모 {disabledCount}건이 있습니다.
|
|
<button
|
|
onClick={() => void onProcessDisabled()}
|
|
style={{
|
|
marginLeft: 8,
|
|
background: '#0a4b80',
|
|
color: '#fff',
|
|
border: 'none',
|
|
borderRadius: 4,
|
|
padding: '4px 10px',
|
|
fontSize: 12,
|
|
cursor: 'pointer'
|
|
}}
|
|
>
|
|
지금 모두 처리
|
|
</button>
|
|
</div>
|
|
)}
|
|
<label style={{ display: 'block', marginBottom: 8, fontSize: 12, color: '#666' }}>
|
|
Endpoint
|
|
<input
|
|
type="text"
|
|
value={endpoint}
|
|
onChange={(e) => setEndpoint(e.target.value)}
|
|
placeholder="http://localhost:11434"
|
|
style={{
|
|
display: 'block',
|
|
width: '100%',
|
|
padding: '6px 8px',
|
|
marginTop: 4,
|
|
fontSize: 13,
|
|
border: '1px solid #ccc',
|
|
borderRadius: 4
|
|
}}
|
|
/>
|
|
</label>
|
|
<label style={{ display: 'block', marginBottom: 8, fontSize: 12, color: '#666' }}>
|
|
Model
|
|
<input
|
|
type="text"
|
|
value={model}
|
|
onChange={(e) => setModel(e.target.value)}
|
|
placeholder="gemma2:2b"
|
|
style={{
|
|
display: 'block',
|
|
width: '100%',
|
|
padding: '6px 8px',
|
|
marginTop: 4,
|
|
fontSize: 13,
|
|
border: '1px solid #ccc',
|
|
borderRadius: 4
|
|
}}
|
|
/>
|
|
</label>
|
|
{error && (
|
|
<div style={{ color: '#c33', fontSize: 12, marginBottom: 8 }}>{error}</div>
|
|
)}
|
|
{saveResult && (
|
|
<div style={{ fontSize: 12, marginBottom: 8, color: '#0a4b80' }}>{saveResult}</div>
|
|
)}
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<button
|
|
onClick={() => void onSave()}
|
|
style={{
|
|
background: '#0a4b80',
|
|
color: '#fff',
|
|
border: 'none',
|
|
borderRadius: 4,
|
|
padding: '6px 14px',
|
|
fontSize: 12,
|
|
cursor: 'pointer'
|
|
}}
|
|
>
|
|
저장
|
|
</button>
|
|
<button
|
|
onClick={() => void onRecheck()}
|
|
style={{
|
|
background: 'transparent',
|
|
color: '#0a4b80',
|
|
border: '1px solid #0a4b80',
|
|
borderRadius: 4,
|
|
padding: '6px 14px',
|
|
fontSize: 12,
|
|
cursor: 'pointer'
|
|
}}
|
|
>
|
|
지금 재확인
|
|
</button>
|
|
</div>
|
|
{recheckResult && (
|
|
<div style={{ fontSize: 12, marginTop: 8 }}>{recheckResult}</div>
|
|
)}
|
|
<VisionSection />
|
|
</div>
|
|
);
|
|
}
|