/* screens.jsx — экраны приложения: Записи, Поиск, Напоминания, Вход. */ function EmptyState({ icon = "inbox", title, sub, action }) { return (

{title}

{sub &&

{sub}

} {action &&
{action}
}
); } function NotesScreen({ onOpenNote, onNew, reloadKey }) { const cats = Store.useCategories(); const [notes, setNotes] = React.useState([]); const [loading, setLoading] = React.useState(true); const [category, setCategory] = React.useState("all"); const [q, setQ] = React.useState(""); const [activeTag, setActiveTag] = React.useState(null); const [view, setView] = React.useState("active"); // active | archived | trash const load = React.useCallback(async () => { setLoading(true); const data = await api.getNotes({ category, q: q || undefined, tag: activeTag || undefined, view }); setNotes(data); setLoading(false); }, [category, q, activeTag, view]); React.useEffect(() => { load(); }, [category, activeTag, view, reloadKey]); React.useEffect(() => { const t = setTimeout(load, 250); return () => clearTimeout(t); }, [q]); const allTags = React.useMemo(() => { const s = new Set(); notes.forEach((n) => n.tags.forEach((t) => s.add(t))); return [...s]; }, [notes]); const onAction = async (note, action) => { if (action === "pin") await api.pinNote(note.id, !note.pinned); else if (action === "archive") await api.archiveNote(note.id, true); else if (action === "unarchive") await api.archiveNote(note.id, false); else if (action === "trash") await api.deleteNote(note.id); else if (action === "restore") await api.restoreNote(note.id); else if (action === "purge") await api.purgeNote(note.id); load(); }; const emptyTrash = async () => { await api.emptyTrash(); load(); }; const chips = [{ key: "all", label: "Все", color: null }, ...cats]; const filtering = q || category !== "all" || activeTag; // группировка по датам (для активных) const groups = React.useMemo(() => groupByDate(notes, view), [notes, view]); const viewTabs = [ { v: "active", label: "Активные", icon: "notes" }, { v: "archived", label: "Архив", icon: "inbox" }, { v: "trash", label: "Корзина", icon: "trash" }, ]; return (
{/* вид: активные / архив / корзина */}
{viewTabs.map((t) => ( ))}
{view === "trash" && notes.length > 0 && ( )}
{view === "trash" && (

Удалённое хранится 30 дней, потом исчезает само.

)}
setQ(e.target.value)} placeholder="Быстрый поиск по тексту…" className="w-full h-11 rounded-xl border border-[var(--border)] bg-[var(--surface)] pl-10 pr-3 text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition placeholder:text-[var(--muted)]" />
{chips.map((c) => { const active = category === c.key; return ( ); })}
{allTags.length > 0 && (
Теги: {activeTag && ( )} {!activeTag && allTags.slice(0, 10).map((t) => ( ))}
)} {loading ? (
{[0,1,2,3].map((i) => )}
) : notes.length === 0 ? ( Добавить запись} /> ) : (
{groups.map((g) => (
{g.label &&

{g.label}

}
{g.items.map((n, i) => )}
))}
)}
); } // группировка ленты по датам: Закреплённые / Сегодня / Вчера / На этой неделе / Раньше function groupByDate(notes, view) { if (view !== "active") return [{ key: "all", label: null, items: notes }]; const pinned = notes.filter((n) => n.pinned); const rest = notes.filter((n) => !n.pinned); const buckets = { today: [], yesterday: [], week: [], older: [] }; const now = new Date(); const sod = (d) => { const x = new Date(d); x.setHours(0, 0, 0, 0); return x; }; rest.forEach((n) => { const diff = Math.round((sod(now) - sod(n.created_at)) / 86400e3); if (diff <= 0) buckets.today.push(n); else if (diff === 1) buckets.yesterday.push(n); else if (diff < 7) buckets.week.push(n); else buckets.older.push(n); }); const out = []; if (pinned.length) out.push({ key: "pinned", label: "📌 Закреплённые", items: pinned }); if (buckets.today.length) out.push({ key: "today", label: "Сегодня", items: buckets.today }); if (buckets.yesterday.length) out.push({ key: "yesterday", label: "Вчера", items: buckets.yesterday }); if (buckets.week.length) out.push({ key: "week", label: "На этой неделе", items: buckets.week }); if (buckets.older.length) out.push({ key: "older", label: "Раньше", items: buckets.older }); return out; } function SearchScreen({ onOpenNote }) { const [mode, setMode] = React.useState("search"); const [query, setQuery] = React.useState(""); const [state, setState] = React.useState("idle"); const [res, setRes] = React.useState({ answer: null, results: [] }); const ref = React.useRef(null); React.useEffect(() => { ref.current && ref.current.focus(); }, []); const run = async (qOverride) => { const text = ((qOverride != null ? qOverride : query) || "").trim(); if (!text) return; if (text !== query) setQuery(text); setState("loading"); const data = await api.search({ query: text }); setRes(data); setState("done"); }; const suggestions = ["что у меня по работе на этой неделе", "напоминания про здоровье", "идеи для продукта", "что купить"]; return (
{[{ v: "search", label: "Поиск", icon: "search" }, { v: "chat", label: "Чат с заметками", icon: "sparkles" }].map((t) => ( ))}
{mode === "chat" ? : (
setQuery(e.target.value)} onKeyDown={(e) => e.key === "Enter" && run()} placeholder="Спроси или найди по смыслу…" className="flex-1 bg-transparent outline-none py-1.5 text-[15px] sm:text-base text-[var(--text)] placeholder:text-[var(--muted)]" />
{state === "idle" && (
Например: {suggestions.map((s) => ( ))}
)} {state === "loading" && (
{[0,1].map((i) => )}
)} {state === "done" && (
{res.answer && (
Ответ ИИ

{res.answer}

)} {res.results.length === 0 ? ( ) : ( <>

Найдено записей: {res.results.length}

{res.results.map((n, i) => )}
)}
)}
)}
); } /* ----------------------------------------------------------- ЧАТ С ЗАМЕТКАМИ */ function ChatView({ onOpenNote }) { const [messages, setMessages] = React.useState([ { role: "ai", text: "Привет! Я знаю всё, что ты записал. Спроси, например: «что у меня по работе?» или «собери все идеи».", sources: [] }, ]); const [input, setInput] = React.useState(""); const [busy, setBusy] = React.useState(false); const endRef = React.useRef(null); React.useEffect(() => { if (endRef.current) endRef.current.scrollIntoView({ block: "nearest" }); }, [messages, busy]); const send = async (text) => { const msg = (text || input).trim(); if (!msg || busy) return; setInput(""); setMessages((m) => [...m, { role: "user", text: msg }]); setBusy(true); const res = await api.chat({ message: msg, history: messages }); setMessages((m) => [...m, { role: "ai", text: res.answer, sources: res.sources || [] }]); setBusy(false); }; const chips = ["что у меня по работе?", "собери все идеи", "что нужно купить", "напомни про здоровье"]; return (
{messages.map((m, i) => (

{m.text}

{m.sources && m.sources.length > 0 && (
{m.sources.map((s) => ( ))}
)}
))} {busy && (
)}
{messages.length <= 1 && (
{chips.map((c) => ( ))}
)}
setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && send()} placeholder="Спроси по своим заметкам…" className="flex-1 bg-transparent outline-none py-1.5 text-[15px] text-[var(--text)] placeholder:text-[var(--muted)]" />
); } function RemindersScreen({ onOpenNote, reloadKey, onChanged }) { const [items, setItems] = React.useState([]); const [loading, setLoading] = React.useState(true); const load = React.useCallback(async () => { setLoading(true); const data = await api.getReminders(); setItems(data); setLoading(false); }, []); React.useEffect(() => { load(); }, [reloadKey]); const upcoming = items.filter((r) => r.status === "pending").sort((a, b) => new Date(a.remind_at) - new Date(b.remind_at)); const past = items.filter((r) => r.status !== "pending").sort((a, b) => new Date(b.remind_at) - new Date(a.remind_at)); const cancel = async (r) => { await api.updateReminder(r.id, { status: "cancelled" }); await load(); onChanged && onChanged(); }; const done = async (r) => { await api.updateReminder(r.id, { status: "done" }); await load(); onChanged && onChanged(); }; const snooze = async (r, days) => { const base = new Date(r.remind_at); const now = new Date(); const from = base > now ? base : now; from.setDate(from.getDate() + days); await api.updateReminder(r.id, { remind_at: from.toISOString(), status: "pending" }); await load(); onChanged && onChanged(); }; const openNote = async (r) => { const list = await api.getNotes({}); const n = list.find((x) => x.id === r.note_id); if (n) onOpenNote(n); }; return (
{loading ? (
{[0,1,2].map((i) =>
)}
) : items.length === 0 ? ( ) : (
{past.length > 0 && }
)}
); } function ReminderGroup({ title, count, items, onCancel, onDone, onSnooze, onOpen, muted }) { if (items.length === 0) return (

Пусто

); return (
{items.map((r) => )}
); } function GroupHead({ title, count }) { return (

{title}

{count}
); } function ReminderCard({ r, onCancel, onDone, onSnooze, onOpen, muted }) { const f = formatRemind(r.remind_at); const done = r.status !== "pending"; const label = r.status === "cancelled" ? "Отменено" : r.status === "done" ? "Выполнено · " + f.text : r.status === "sent" ? "Отправлено · " + f.text : f.text; const icon = r.status === "cancelled" ? "close" : done ? "check" : "bell"; return (

{r.text}

{label}
{r.status === "pending" && (
)}
); } function LoginScreen({ onLogin }) { const [config, setConfig] = React.useState(null); const [busy, setBusy] = React.useState(false); const [error, setError] = React.useState(""); const [mode, setMode] = React.useState("tg"); // tg | login const [uname, setUname] = React.useState(""); const [pwd, setPwd] = React.useState(""); const widgetRef = React.useRef(null); React.useEffect(() => { api.getConfig().then(setConfig).catch(() => setConfig({ bot_username: null })); }, []); React.useEffect(() => { window.onTelegramAuth = async (user) => { setError(""); setBusy(true); try { await api.authTelegram(user); onLogin(); } catch (e) { setError(e && e.status === 403 ? "У этого аккаунта нет доступа." : "Не удалось войти. Попробуй ещё раз."); setBusy(false); } }; return () => { delete window.onTelegramAuth; }; }, [onLogin]); React.useEffect(() => { if (!config || !config.bot_username || !widgetRef.current) return; widgetRef.current.innerHTML = ""; const s = document.createElement("script"); s.async = true; s.src = "https://telegram.org/js/telegram-widget.js?22"; s.setAttribute("data-telegram-login", config.bot_username); s.setAttribute("data-size", "large"); s.setAttribute("data-radius", "12"); s.setAttribute("data-onauth", "onTelegramAuth(user)"); s.setAttribute("data-request-access", "write"); widgetRef.current.appendChild(s); }, [config]); const demo = async () => { setBusy(true); try { await api.authTelegram({ demo: true }); onLogin(); } catch (e) { setError("Демо-вход недоступен."); setBusy(false); } }; const doLogin = async () => { if (!uname.trim() || !pwd) return; setError(""); setBusy(true); try { await api.login({ username: uname.trim(), password: pwd }); onLogin(); } catch (e) { setError(e && e.status === 401 ? "Неверный логин или пароль." : "Не удалось войти."); setBusy(false); } }; return (
Умный блокнот

Умный блокнот

Скидывай мысли, задачи и заметки — ИИ сам разложит их по категориям, поставит теги и напомнит вовремя.

{mode === "tg" ? ( <>
{busy && } {!busy && config && config.bot_username &&
} {!busy && config && !config.bot_username && ( )} {!config && }
) : (
setUname(e.target.value)} placeholder="логин" autoCapitalize="none" className="w-full rounded-xl border border-[var(--border)] bg-[var(--surface-2)] px-3.5 h-12 text-[15px] text-[var(--text)] outline-none focus:border-[var(--accent)] transition" /> setPwd(e.target.value)} onKeyDown={(e) => e.key === "Enter" && doLogin()} placeholder="пароль" className="w-full rounded-xl border border-[var(--border)] bg-[var(--surface-2)] px-3.5 h-12 text-[15px] text-[var(--text)] outline-none focus:border-[var(--accent)] transition" />
)} {error &&

{error}

}

Логин/пароль задаётся в настройках после входа через Telegram.

); } function ScreenHead({ title, sub, children }) { return (

{title}

{sub &&

{sub}

}
{children &&
{children}
}
); } Object.assign(window, { NotesScreen, SearchScreen, RemindersScreen, LoginScreen, EmptyState, ScreenHead, ReminderCard });