Files
inkling/src/renderer/inbox/components/IntentBanner.tsx
altair823 7cdc1e1104 feat(inbox): IntentBanner with rotating prompts (Strategy §2.2)
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>
2026-04-25 12:17:53 +09:00

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>
);
}