mirror of
https://github.com/karpathy/nanochat.git
synced 2026-05-10 18:00:17 +00:00
* feat(inference): deploy d24 SFT weights to Modal Repoint Modal inference app from the broken d20 checkpoint to our own ManmohanSharma/nanochat-d24 SFT step 484. Rewrites the standalone model as an inference-only port of nanochat/gpt.py so the modern architecture (smear gate, per-layer value embeddings, ve_gate, backout, sliding window attention via SDPA, rotary base 100000, padded vocab, logit softcap) loads cleanly from the checkpoint. Tokenizer loads the pickled tiktoken encoding directly so special tokens end up at their true IDs (32759-32767), and the stop check uses that set instead of hardcoded 0-8. GPU bumped to L4 for headroom. HF token sourced from the 'huggingface' Modal secret. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(frontend): polished redesign with serif display + dark mode Lifts the craft level of the landing and chat UI without changing the desi identity. Adds Fraunces for display headlines, a floating pill LandingNav, a saffron-glow hero with a large serif headline and black pill CTAs, and three gradient-tiled feature cards with inline SVG glyphs replacing the emoji cards. The chat empty state is now a serif greeting with pill-chip prompt starters, and ChatInput is a single rounded pod so the send button sits inside the input (fixes the misaligned floating button). Adds a class-based dark mode across the chat surfaces with a sun/moon toggle in the sidebar footer, powered by a small useTheme hook and a no-flash init script in the root layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(frontend): add ESLint config so CI lint step passes next lint was failing with an interactive prompt because the repo had no ESLint config. Adds a minimal next/core-web-vitals extends and drops the now-unloadable @typescript-eslint/no-explicit-any disable directive in the stream proxy by narrowing the body type to unknown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
190 lines
7.9 KiB
TypeScript
190 lines
7.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import {
|
|
Plus,
|
|
PanelLeftClose,
|
|
PanelLeftOpen,
|
|
LogOut,
|
|
ChevronDown,
|
|
Trash2,
|
|
Sun,
|
|
Moon,
|
|
} from 'lucide-react';
|
|
import SamosaLogo from '@/components/svg/SamosaLogo';
|
|
import { useChatStore, groupConversations, MODEL_OPTIONS } from '@/store/chatStore';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
import { useTheme } from '@/hooks/useTheme';
|
|
import clsx from 'clsx';
|
|
|
|
export default function Sidebar() {
|
|
const { user, logout } = useAuth();
|
|
const { theme, toggle } = useTheme();
|
|
const {
|
|
conversations,
|
|
currentConversationId,
|
|
sidebarOpen,
|
|
model,
|
|
setModel,
|
|
toggleSidebar,
|
|
createConversation,
|
|
selectConversation,
|
|
deleteConversation,
|
|
fetchConversations,
|
|
} = useChatStore();
|
|
|
|
useEffect(() => {
|
|
fetchConversations();
|
|
}, [fetchConversations]);
|
|
|
|
const grouped = groupConversations(conversations);
|
|
|
|
return (
|
|
<aside
|
|
className={clsx(
|
|
'flex flex-col bg-cream-light dark:bg-ink-soft border-r border-cream-border dark:border-ink-border transition-all duration-300 ease-in-out overflow-hidden',
|
|
sidebarOpen ? 'w-[260px]' : 'w-0 md:w-[56px]',
|
|
)}
|
|
>
|
|
<div className="flex items-center justify-between px-3 py-3 border-b border-cream-border dark:border-ink-border">
|
|
<Link href="/" className={clsx('flex items-center gap-2 overflow-hidden', !sidebarOpen && 'md:hidden')}>
|
|
<SamosaLogo size={28} />
|
|
<span className="font-display font-semibold text-base text-gray-900 dark:text-ink-text whitespace-nowrap tracking-tight">
|
|
samosaChaat
|
|
</span>
|
|
</Link>
|
|
<button
|
|
aria-label="Toggle sidebar"
|
|
onClick={toggleSidebar}
|
|
className="p-1.5 rounded-md hover:bg-cream dark:hover:bg-ink-elev text-brown-light dark:text-ink-text-soft"
|
|
>
|
|
{sidebarOpen ? <PanelLeftClose size={18} /> : <PanelLeftOpen size={18} />}
|
|
</button>
|
|
</div>
|
|
|
|
{sidebarOpen && (
|
|
<>
|
|
<div className="px-3 py-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => createConversation()}
|
|
className="w-full flex items-center justify-center gap-2 px-3 py-2.5 rounded-full bg-gray-900 dark:bg-ink-text text-white dark:text-ink text-sm font-medium hover:-translate-y-px shadow-[0_6px_20px_rgba(0,0,0,0.18)] transition-all"
|
|
>
|
|
<Plus size={16} />
|
|
New chat
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto px-2 nice-scrollbar">
|
|
{conversations.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center h-full px-4 text-center py-12">
|
|
<div className="text-3xl mb-3 opacity-30">💬</div>
|
|
<p className="text-sm text-gray-400 dark:text-ink-text-soft font-medium">No conversations yet.</p>
|
|
<p className="text-xs text-gray-400 dark:text-ink-text-soft mt-1">Start your first chat!</p>
|
|
</div>
|
|
) : (
|
|
Object.entries(grouped).map(([group, items]) => {
|
|
if (items.length === 0) return null;
|
|
return (
|
|
<div key={group} className="mb-4">
|
|
<div className="px-2 mb-1 text-[11px] uppercase tracking-wider text-gray-400 dark:text-ink-text-soft font-medium">
|
|
{group}
|
|
</div>
|
|
<ul className="space-y-0.5">
|
|
{items.map((c) => (
|
|
<li key={c.id} className="group relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => selectConversation(c.id)}
|
|
className={clsx(
|
|
'w-full text-left px-3 py-2 rounded-full text-sm truncate transition-colors pr-9',
|
|
c.id === currentConversationId
|
|
? 'bg-cream dark:bg-ink-elev text-brown dark:text-ink-text font-medium'
|
|
: 'text-gray-700 dark:text-ink-text-soft hover:bg-cream/70 dark:hover:bg-ink-elev/70',
|
|
)}
|
|
title={c.title}
|
|
>
|
|
{c.title}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
deleteConversation(c.id);
|
|
}}
|
|
className="absolute right-1.5 top-1/2 -translate-y-1/2 p-1 rounded opacity-0 group-hover:opacity-100 hover:bg-cream dark:hover:bg-ink-elev text-gray-400 hover:text-chutney-red transition-all"
|
|
aria-label={`Delete ${c.title}`}
|
|
>
|
|
<Trash2 size={14} />
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
<div className="px-3 py-3 border-t border-cream-border dark:border-ink-border space-y-3">
|
|
<div>
|
|
<label htmlFor="model-select" className="block text-[11px] uppercase tracking-wider text-gray-400 dark:text-ink-text-soft mb-1">
|
|
Model
|
|
</label>
|
|
<div className="relative">
|
|
<select
|
|
id="model-select"
|
|
value={model}
|
|
onChange={(e) => setModel(e.target.value)}
|
|
className="w-full appearance-none px-3 py-2 pr-8 rounded-xl border border-cream-border dark:border-ink-border bg-white dark:bg-ink text-sm text-gray-800 dark:text-ink-text focus:outline-none focus:border-saffron"
|
|
>
|
|
{MODEL_OPTIONS.map((m) => (
|
|
<option key={m.id} value={m.id}>
|
|
{m.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<ChevronDown
|
|
size={14}
|
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 dark:text-ink-text-soft pointer-events-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 pt-1">
|
|
<div className="h-9 w-9 rounded-full bg-gradient-to-br from-saffron to-gold text-white flex items-center justify-center text-sm font-semibold shadow-sm">
|
|
{(user?.name ?? 'G')[0].toUpperCase()}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium text-gray-800 dark:text-ink-text truncate">
|
|
{user?.name ?? 'Guest'}
|
|
</div>
|
|
<div className="text-xs text-gray-500 dark:text-ink-text-soft truncate">
|
|
{user?.email ?? 'Not signed in'}
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
onClick={toggle}
|
|
className="p-1.5 rounded-md hover:bg-cream dark:hover:bg-ink-elev text-gray-500 dark:text-ink-text-soft hover:text-brown dark:hover:text-ink-text transition-colors"
|
|
>
|
|
{theme === 'dark' ? <Sun size={16} /> : <Moon size={16} />}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
aria-label="Sign out"
|
|
onClick={logout}
|
|
className="p-1.5 rounded-md hover:bg-cream dark:hover:bg-ink-elev text-gray-500 dark:text-ink-text-soft hover:text-brown dark:hover:text-ink-text transition-colors"
|
|
>
|
|
<LogOut size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</aside>
|
|
);
|
|
}
|