Task 27 of the slice plan. Surfaces 'meaning question' once per note (gated by intent_prompted_at IS NULL on the backend side; frontend just provides the input UX). intentPrompts.ts holds the 4 rotating prompts plus a deterministic pickIntentPrompt(noteId) (FNV-style 32-bit hash mod 4) so the same note always gets the same question across reloads. Submit calls setIntent and reports the typed text up; Skip calls dismissIntent and reports null. 200-char cap matches repo-side truncation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
58 lines
2.0 KiB
TypeScript
58 lines
2.0 KiB
TypeScript
import React, { useState, useCallback } from 'react';
|
|
import { inboxApi } from '../api.js';
|
|
import { pickIntentPrompt } from '@shared/intentPrompts';
|
|
|
|
interface Props {
|
|
noteId: string;
|
|
onResolved: (intentText: string | null) => void;
|
|
}
|
|
|
|
export function IntentBanner({ noteId, onResolved }: Props): React.ReactElement {
|
|
const [draft, setDraft] = useState('');
|
|
const [busy, setBusy] = useState(false);
|
|
const prompt = pickIntentPrompt(noteId);
|
|
|
|
const submit = useCallback(async () => {
|
|
const text = draft.trim();
|
|
if (text.length === 0) return;
|
|
setBusy(true);
|
|
try {
|
|
await inboxApi.setIntent(noteId, text);
|
|
onResolved(text);
|
|
} finally { setBusy(false); }
|
|
}, [draft, noteId, onResolved]);
|
|
|
|
const skip = useCallback(async () => {
|
|
setBusy(true);
|
|
try {
|
|
await inboxApi.dismissIntent(noteId);
|
|
onResolved(null);
|
|
} finally { setBusy(false); }
|
|
}, [noteId, onResolved]);
|
|
|
|
return (
|
|
<div style={{ marginTop: 8, padding: 10, background: '#fff8d6', borderRadius: 8, border: '1px solid #f1d97c' }}>
|
|
<div style={{ fontSize: 12, color: '#7a5a00', marginBottom: 6 }}>💭 {prompt}</div>
|
|
<div style={{ display: 'flex', gap: 6 }}>
|
|
<input
|
|
value={draft}
|
|
onChange={(e) => setDraft(e.target.value)}
|
|
onKeyDown={(e) => { if (e.key === 'Enter') void submit(); }}
|
|
placeholder="한 줄 입력 (200자)"
|
|
disabled={busy}
|
|
style={{ flex: 1, border: '1px solid #ddd', borderRadius: 4, padding: '4px 6px', fontSize: 13 }}
|
|
maxLength={200}
|
|
/>
|
|
<button onClick={() => void submit()} disabled={busy || draft.trim().length === 0}
|
|
style={{ background: '#f1d97c', border: 'none', borderRadius: 4, padding: '4px 10px', cursor: 'pointer', fontSize: 12 }}>
|
|
저장
|
|
</button>
|
|
<button onClick={() => void skip()} disabled={busy}
|
|
style={{ background: 'transparent', border: 'none', color: '#7a5a00', cursor: 'pointer', fontSize: 12 }}>
|
|
건너뛰기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|