Chat layout: history in the main sidebar, consistent header, cleaner icon
- Desktop: on /chat the main sidebar shows the chat list (search, New chat, items with rename/delete, active highlight) in place of the deck navigation; the separate second column is gone and the chat uses the full width. The expand/collapse chevron sits outside the swappable section and a collapsed rail stays clean (the highlighted Chat row already opens a new chat). Active conversation travels via a tiny store (lib/chat-active) so the sidebar needs no useSearchParams and pages stay static. - Header (phones): history, theme and New chat are now identical chips - same 44px hit area, rounded-2xl, border/surface tokens, size-5 icons, even spacing; the title block truncates cleanly. ThemeToggle gained a 'plain' variant so the header styles it without touching other pages. - ChatIcon redrawn (clean message-circle, no inner lines) and the empty state uses the app's icon-chip pattern (size-12 chip, accent-600/200) instead of the bare oversized glyph; the model chip icon follows. - Also fixed a pre-existing dev-only breakage found while verifying: the tab-bar bounce animated three keyframes with a spring, which framer motion's dev build throws on - breaking client updates in the dev instance (library tab switching, sheet Escape). It now uses a tween. - Dev metadata keeps the Apple icons in both branches. - SW -> freezmo-v35.
This commit is contained in:
parent
95643fa009
commit
8a3bef0add
9 changed files with 192 additions and 75 deletions
|
|
@ -7,7 +7,7 @@ import Link from "next/link";
|
|||
|
||||
import { Composer, type PendingAttachment } from "./Composer";
|
||||
import { ContextPicker } from "./ContextPicker";
|
||||
import { ChatHistoryList, HistorySheet } from "./HistoryPanel";
|
||||
import { HistorySheet } from "./HistoryPanel";
|
||||
import { MessageRow } from "./MessageRow";
|
||||
import { ModelPicker } from "./ModelPicker";
|
||||
import { ChatIcon, ChevronDownIcon, ChevronLeftIcon, HistoryIcon, PlusIcon, RefreshIcon } from "@/components/icons";
|
||||
|
|
@ -18,6 +18,7 @@ import { ApiError } from "@/lib/client-api";
|
|||
import { extractChatAttachment, streamChat } from "@/lib/chat-client";
|
||||
import { buildChatContext, deckHasNotes } from "@/lib/chat-context";
|
||||
import { takeChatSeed, type ChatSeed } from "@/lib/chat-seed";
|
||||
import { chatActive } from "@/lib/chat-active";
|
||||
import { chatHistoryStore, conversationMessages, deleteConversation, ensureChatsHydrated, getConversation, renameConversation, saveConversation } from "@/lib/chat-store";
|
||||
import {
|
||||
chatMessageId,
|
||||
|
|
@ -123,6 +124,7 @@ export function ChatScreen() {
|
|||
setError(null);
|
||||
if (found.model) setModel(found.model);
|
||||
loadedRef.current = found.id;
|
||||
chatActive.set(found.id);
|
||||
}, []);
|
||||
|
||||
const conversationParam = search.get("c");
|
||||
|
|
@ -240,6 +242,7 @@ export function ChatScreen() {
|
|||
const id = conversationId ?? chatMessageId("chat");
|
||||
if (!conversationId) {
|
||||
setConversationId(id);
|
||||
chatActive.set(id);
|
||||
// Keep the URL in step so a reload continues this conversation.
|
||||
// history.replaceState avoids a router round-trip for the same page.
|
||||
window.history.replaceState(null, "", `/chat?c=${id}`);
|
||||
|
|
@ -403,7 +406,7 @@ export function ChatScreen() {
|
|||
setMessages(messages.slice(0, lastUserIndex));
|
||||
};
|
||||
|
||||
const newChat = () => {
|
||||
const resetView = useCallback(() => {
|
||||
abortRef.current?.abort();
|
||||
loadedRef.current = null;
|
||||
setConversationId(null);
|
||||
|
|
@ -413,10 +416,28 @@ export function ChatScreen() {
|
|||
setAttachments([]);
|
||||
setDraft("");
|
||||
setError(null);
|
||||
setHistoryOpen(false);
|
||||
chatActive.set(null);
|
||||
window.history.replaceState(null, "", "/chat");
|
||||
}, []);
|
||||
|
||||
const newChat = () => {
|
||||
resetView();
|
||||
setHistoryOpen(false);
|
||||
};
|
||||
|
||||
/* The sidebar can start a new chat (URL -> /chat) while this screen is open. */
|
||||
useEffect(() => {
|
||||
if (conversationParam || !loadedRef.current) return;
|
||||
resetView();
|
||||
}, [conversationParam, resetView]);
|
||||
|
||||
/* ...and delete the conversation that is currently open. */
|
||||
useEffect(() => {
|
||||
if (!conversationId || !loadedRef.current) return;
|
||||
if (getConversation(conversationId)) return;
|
||||
resetView();
|
||||
}, [conversations, conversationId, resetView]);
|
||||
|
||||
const openConversation = (id: string) => {
|
||||
abortRef.current?.abort();
|
||||
setHistoryOpen(false);
|
||||
|
|
@ -501,31 +522,15 @@ export function ChatScreen() {
|
|||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 w-full">
|
||||
{/* Desktop: the conversation list lives in its own column. */}
|
||||
<aside className="hidden w-64 shrink-0 flex-col border-r border-[var(--hairline-soft)] bg-[var(--surface-sunken)] p-2 lg:flex">
|
||||
<h2 className="px-1 pb-1 pt-1 text-[12px] font-semibold uppercase tracking-wide text-[var(--text-subtle)]">
|
||||
{t("chat.history")}
|
||||
</h2>
|
||||
<ChatHistoryList
|
||||
conversations={conversations}
|
||||
currentId={conversationId}
|
||||
query={query}
|
||||
onQueryChange={setQuery}
|
||||
onOpen={openConversation}
|
||||
onRename={renameConversation}
|
||||
onDelete={handleDeleteConversation}
|
||||
onNew={newChat}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
{/* Desktop's conversation list lives in the main sidebar (on /chat). */}
|
||||
<div className="relative flex h-full min-h-0 min-w-0 flex-1 flex-col" style={{ paddingBottom: "var(--chat-keyboard, 0px)" }}>
|
||||
<header className="glass sticky top-0 z-30 flex h-14 shrink-0 items-center gap-1.5 px-2 sm:px-4">
|
||||
<header className="glass sticky top-0 z-30 flex h-14 shrink-0 items-center gap-2 px-2 sm:px-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (window.history.length > 1 ? router.back() : router.push("/"))}
|
||||
aria-label={t("chat.back")}
|
||||
title={t("chat.back")}
|
||||
className="grid size-10 shrink-0 place-items-center rounded-2xl text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)] lg:hidden"
|
||||
className="grid size-11 shrink-0 place-items-center rounded-2xl text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)] lg:hidden"
|
||||
>
|
||||
<ChevronLeftIcon className="size-5" />
|
||||
</button>
|
||||
|
|
@ -536,26 +541,31 @@ export function ChatScreen() {
|
|||
{seed ? ` · ${seed.label}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setHistoryOpen(true)}
|
||||
aria-label={t("chat.history")}
|
||||
title={t("chat.history")}
|
||||
className="grid size-10 shrink-0 place-items-center rounded-2xl text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)] lg:hidden"
|
||||
>
|
||||
<HistoryIcon className="size-5" />
|
||||
</button>
|
||||
<ThemeToggle className="hidden lg:grid" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={newChat}
|
||||
aria-label={t("chat.new")}
|
||||
title={t("chat.new")}
|
||||
className="flex h-10 shrink-0 items-center gap-1.5 rounded-2xl border border-[var(--glass-border)] bg-[var(--surface-strong)] px-3 text-[12.5px] font-medium text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
<span className="hidden sm:inline">{t("chat.new")}</span>
|
||||
</button>
|
||||
{/* Actions share one size, radius, border, background and hit area. */}
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setHistoryOpen(true)}
|
||||
aria-label={t("chat.history")}
|
||||
title={t("chat.history")}
|
||||
className="grid size-11 place-items-center rounded-2xl border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)] lg:hidden"
|
||||
>
|
||||
<HistoryIcon className="size-5" />
|
||||
</button>
|
||||
<ThemeToggle
|
||||
plain
|
||||
className="grid size-11 place-items-center rounded-2xl border border-[var(--glass-border)] bg-[var(--surface-strong)]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={newChat}
|
||||
aria-label={t("chat.new")}
|
||||
title={t("chat.new")}
|
||||
className="grid size-11 place-items-center rounded-2xl border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
|
||||
>
|
||||
<PlusIcon className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="relative min-h-0 flex-1">
|
||||
|
|
@ -564,8 +574,10 @@ export function ChatScreen() {
|
|||
{messages.length === 0 ? (
|
||||
<div className="space-y-5 py-6">
|
||||
<div className="text-center">
|
||||
<ChatIcon className="mx-auto size-10 text-accent-400" />
|
||||
<h2 className="mt-3 text-[20px] font-semibold tracking-[-0.01em] text-[var(--text)]">{t("chat.emptyTitle")}</h2>
|
||||
<span className="mx-auto mb-4 grid size-12 place-items-center rounded-2xl border border-[var(--hairline)] bg-[var(--surface-strong)] text-accent-600 dark:text-accent-200">
|
||||
<ChatIcon className="size-6" />
|
||||
</span>
|
||||
<h2 className="text-[20px] font-semibold tracking-[-0.01em] text-[var(--text)]">{t("chat.emptyTitle")}</h2>
|
||||
<p className="mx-auto mt-1.5 max-w-sm text-[13.5px] leading-relaxed text-[var(--text-subtle)]">
|
||||
{t("chat.emptyHint")}
|
||||
</p>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue