(chore) init commit and add tasks
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { Panel } from '@/components/cluster/Panel'
|
||||
import { SlotMeter } from '@/components/cluster/SlotMeter'
|
||||
|
||||
const DAILY_BUDGET = 12
|
||||
|
||||
type Turn = {
|
||||
role: 'user' | 'model'
|
||||
reasoning?: string
|
||||
content: string
|
||||
streaming?: boolean
|
||||
}
|
||||
|
||||
const SEED: Turn[] = [
|
||||
{ role: 'user', content: 'Why is the sms application degraded?' },
|
||||
{
|
||||
role: 'model',
|
||||
reasoning:
|
||||
'The snapshot lists sms with health "degraded" and sync "synced". Sync being clean means the manifests applied fine, so this is a runtime problem rather than a delivery one. The sms namespace shows 0 of 1 pods running. A single pod that never reaches Running, with Argo reporting the desired state as applied, usually means the pod cannot be placed at all.',
|
||||
content:
|
||||
'Delivery is fine — sms is Synced, so Argo applied everything it was asked to. The failure is at runtime: the namespace has one pod and zero running. Argo marks the app degraded because the workload never became ready, not because the manifests are wrong.',
|
||||
},
|
||||
]
|
||||
|
||||
const REPLY =
|
||||
'Wave 4 is empty. Waves are sort keys, not a sequence — Argo orders by value and skips gaps, so 3 is followed directly by 5. Nothing is missing.'
|
||||
|
||||
const REPLY_REASONING =
|
||||
'The user is asking about a gap in the wave column display. Argo CD sync waves are integers used purely for ordering; there is no requirement that they be contiguous. The cluster uses 0,1,2,3,5,6,7,8. This is normal and usually happens when a wave is retired or intentionally reserved.'
|
||||
|
||||
export default function ChatPage() {
|
||||
const [turns, setTurns] = useState<Turn[]>(SEED)
|
||||
const [value, setValue] = useState('')
|
||||
const [queue, setQueue] = useState<number | null>(null)
|
||||
const [slots, setSlots] = useState(3)
|
||||
const [used, setUsed] = useState(2)
|
||||
const [openReasoning, setOpenReasoning] = useState<number | null>(1)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' })
|
||||
}, [turns, queue])
|
||||
|
||||
const send = () => {
|
||||
if (!value.trim() || queue !== null || used >= DAILY_BUDGET) return
|
||||
setTurns((t) => [...t, { role: 'user', content: value }])
|
||||
setValue('')
|
||||
setQueue(3)
|
||||
}
|
||||
|
||||
// Placeholder for the queue → stream transition. Real version is SSE from atlas.
|
||||
useEffect(() => {
|
||||
if (queue === null) return
|
||||
if (queue > 0) {
|
||||
const t = setTimeout(() => setQueue((q) => (q === null ? null : q - 1)), 700)
|
||||
return () => clearTimeout(t)
|
||||
}
|
||||
setQueue(null)
|
||||
setSlots((s) => Math.min(s + 1, 6))
|
||||
setUsed((u) => u + 1)
|
||||
setTurns((t) => [...t, { role: 'model', reasoning: REPLY_REASONING, content: '', streaming: true }])
|
||||
|
||||
let i = 0
|
||||
const stream = setInterval(() => {
|
||||
i += 3
|
||||
setTurns((t) => {
|
||||
const next = [...t]
|
||||
const last = next[next.length - 1]
|
||||
if (last?.role !== 'model') return t
|
||||
next[next.length - 1] = { ...last, content: REPLY.slice(0, i), streaming: i < REPLY.length }
|
||||
return next
|
||||
})
|
||||
if (i >= REPLY.length) {
|
||||
clearInterval(stream)
|
||||
setSlots((s) => Math.max(s - 1, 1))
|
||||
}
|
||||
}, 40)
|
||||
return () => clearInterval(stream)
|
||||
}, [queue])
|
||||
|
||||
const exhausted = used >= DAILY_BUDGET
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<header className="border border-wire bg-deck p-6 md:p-8">
|
||||
<p className="font-data text-[10px] uppercase tracking-[0.2em] text-dim">Surface D — Chat</p>
|
||||
<h1 className="mt-3 max-w-3xl font-signage text-3xl font-semibold leading-[1.1] tracking-signage md:text-5xl">
|
||||
A 32B model, running on one card, <span className="text-lamp">two rooms from here.</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-2xl font-plex text-sm leading-relaxed text-dim">
|
||||
It reads a redacted snapshot of the cluster and answers questions about it. It cannot query anything, run
|
||||
anything, or change anything. When all six public slots are busy, you wait in line — the queue is real,
|
||||
and so is the hardware.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-wrap items-center gap-6">
|
||||
<SlotMeter used={slots} size="lg" showCap />
|
||||
<span className="font-data text-[10px] uppercase tracking-[0.14em] text-dim">
|
||||
{DAILY_BUDGET - used} of {DAILY_BUDGET} messages left today
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_260px]">
|
||||
<Panel label="Session" hint="anonymous">
|
||||
<div ref={scrollRef} className="h-[440px] space-y-5 overflow-y-auto pr-1">
|
||||
{turns.map((turn, i) =>
|
||||
turn.role === 'user' ? (
|
||||
<div key={i} className="flex justify-end">
|
||||
<p className="max-w-[85%] border border-wire-lit bg-riser px-3 py-2 font-plex text-sm text-chalk">
|
||||
{turn.content}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div key={i} className="max-w-[92%] space-y-2">
|
||||
{turn.reasoning && (
|
||||
<div className="border-l-2 border-lamp/50">
|
||||
<button
|
||||
onClick={() => setOpenReasoning(openReasoning === i ? null : i)}
|
||||
aria-expanded={openReasoning === i}
|
||||
className="flex w-full items-center gap-2 py-1 pl-3 font-data text-[10px] uppercase tracking-[0.16em] text-lamp focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lamp"
|
||||
>
|
||||
<ChevronDown
|
||||
size={12}
|
||||
className={`transition-transform ${openReasoning === i ? '' : '-rotate-90'}`}
|
||||
/>
|
||||
thinking
|
||||
</button>
|
||||
{openReasoning === i && (
|
||||
<p className="py-1 pl-3 pr-2 font-data text-[11px] leading-relaxed text-dim">
|
||||
{turn.reasoning}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="font-plex text-sm leading-relaxed text-chalk">
|
||||
{turn.content}
|
||||
{turn.streaming && <span className="ml-0.5 inline-block h-4 w-2 animate-pulse bg-lamp align-text-bottom" />}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
{queue !== null && (
|
||||
<div className="flex items-center gap-3 border border-lamp/40 px-3 py-2">
|
||||
<SlotMeter used={6} />
|
||||
<p className="font-data text-[11px] text-lamp">
|
||||
all six public slots busy · position {queue} in queue
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
send()
|
||||
}}
|
||||
className="mt-3 flex items-center gap-2 border-t border-wire pt-3"
|
||||
>
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
disabled={exhausted}
|
||||
aria-label="Message"
|
||||
placeholder={exhausted ? 'Daily limit reached — resets in 14h' : 'Ask about the cluster'}
|
||||
className="min-w-0 flex-1 bg-transparent font-plex text-sm text-chalk placeholder:text-dim/60 focus:outline-none disabled:cursor-not-allowed"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={exhausted || queue !== null}
|
||||
className="border border-lamp px-3 py-1 font-data text-[10px] uppercase tracking-[0.14em] text-lamp transition-colors hover:bg-lamp hover:text-void focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lamp disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-lamp"
|
||||
>
|
||||
send
|
||||
</button>
|
||||
</form>
|
||||
</Panel>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Panel label="Limits" hint="tier 3">
|
||||
<dl className="space-y-2.5 font-data text-[11px]">
|
||||
{[
|
||||
['public slots', '6 of 8'],
|
||||
['queue depth', '20'],
|
||||
['per session', '12 / day'],
|
||||
['request timeout', '120s'],
|
||||
['output cap', '1500 tokens'],
|
||||
].map(([k, v]) => (
|
||||
<div key={k} className="flex items-baseline gap-2">
|
||||
<dt className="text-dim">{k}</dt>
|
||||
<dd className="ml-auto tabular-nums text-chalk">{v}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<p className="mt-4 border-t border-wire pt-3 font-plex text-[11px] leading-relaxed text-dim">
|
||||
Closing this tab frees your slot immediately. Two slots stay reserved so the operator is never locked
|
||||
out of their own hardware.
|
||||
</p>
|
||||
</Panel>
|
||||
|
||||
<Panel label="Cannot do" hint="by construction">
|
||||
<ul className="space-y-1.5 font-data text-[10px] text-dim">
|
||||
{['call tools', 'query the cluster', 'read logs', 'change anything', 'see Secret contents'].map((x) => (
|
||||
<li key={x} className="flex items-center gap-2">
|
||||
<span className="h-px w-3 bg-wire-lit" />
|
||||
{x}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user