/* 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" ?
: (
{state === "idle" && (
Например:
{suggestions.map((s) => (
))}
)}
{state === "loading" && (
)}
{state === "done" && (
{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 ? (
) : items.length === 0 ? (
) : (
{past.length > 0 && }
)}
);
}
function ReminderGroup({ title, count, items, onCancel, onDone, onSnooze, onOpen, muted }) {
if (items.length === 0)
return (
);
return (
);
}
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 (
);
}
Object.assign(window, { NotesScreen, SearchScreen, RemindersScreen, LoginScreen, EmptyState, ScreenHead, ReminderCard });