feat(dashboard): chat-context streaming, telegram thread card, digest/sidebar tweaks, file-tasks route

This commit is contained in:
Manohar 2026-06-05 19:49:23 +00:00
parent 84aa98dd14
commit 0970160f29
6 changed files with 110 additions and 64 deletions

View file

@ -36,6 +36,20 @@ async function persistMessage(role: "user" | "agent", content: string, sessionKe
}
}
export async function GET(request: NextRequest) {
const sessionKey = request.nextUrl.searchParams.get("sessionKey") || DEFAULT_SESSION_KEY;
try {
const res = await fetch(`${BRIDGE_URL}/tiger/chat/history?sessionId=${encodeURIComponent(sessionKey)}&limit=50`, {
headers: { Authorization: `Bearer ${BRIDGE_TOKEN}` },
});
const data = await res.json();
return Response.json(data);
} catch (err) {
return Response.json({ error: (err as Error).message }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
const body = await request.json().catch(() => ({}));
const message: string = body?.message;
@ -47,8 +61,7 @@ export async function POST(request: NextRequest) {
});
}
// Persist user message NOW, before the LLM call. If the call fails, the
// history still records what the user said.
// Persist user message NOW, before the LLM call
await persistMessage("user", message, sessionKey);
const t0 = Date.now();

View file

@ -15,11 +15,17 @@ export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const section = searchParams.get("section"); // active | completed | all
const project = searchParams.get("project"); // dashboard, oil, etc
let endpoint = "/tiger/file-tasks";
if (section === "active") endpoint = "/tiger/file-tasks/active";
else if (section === "completed") endpoint = "/tiger/file-tasks/completed";
// Add project filter if provided
if (project) {
endpoint += `?project=${encodeURIComponent(project)}`;
}
const result = await bridgeGet(endpoint);
return NextResponse.json(result);
} catch (err: unknown) {

View file

@ -53,6 +53,7 @@ const navMain = [
{ title: "Agents", url: "/agents", icon: Bot },
{ title: "Knowledge", url: "/knowledge", icon: Brain },
{ title: "Workspace", url: "/workspace", icon: FolderOpen },
{ title: "Activity", url: "/activity", icon: ScrollText },
{ title: "Cost", url: "/cost", icon: DollarSign },
{ title: "Logs", url: "/logs", icon: ScrollText },
]

View file

@ -1,5 +1,7 @@
"use client"
import { bridgeGet } from "@/lib/bridge"
/**
* digest-card.tsx Today's digest of agent activity
*
@ -27,7 +29,7 @@ interface ActivityResponse {
events: ActivityEvent[]
}
const fetcher = (url: string) => fetch(url).then((r) => r.json())
const fetcher = (url: string) => bridgeGet(url).then((r) => r.json())
function relTime(ts: number): string {
if (!ts) return ""

View file

@ -1,71 +1,93 @@
"use client"
/**
* telegram-thread-card.tsx Preview of recent Telegram conversation
*
* PHASE 1: Renders an empty state today. Phase 4 of the plan adds a Telegram
* listener in the bridge that writes inbound/outbound Telegram messages
* with session_id = "telegram:<chat_id>". When that lands, this component
* starts populating with zero additional frontend work.
*/
import * as React from "react"
import useSWR from "swr"
import { useEffect, useState } from "react"
import { Card } from "@/components/ui/card"
import { Send } from "lucide-react"
interface TelegramMessage {
role: "user" | "agent" | "system"
role: string
content: string
ts: number
}
interface TelegramResponse {
ok: boolean
messages: TelegramMessage[]
}
const fetcher = (url: string) =>
fetch(url).then((r) => (r.ok ? r.json() : { ok: false, messages: [] }))
function relTime(ts: number): string {
if (!ts) return ""
const diff = Date.now() - ts
const m = Math.floor(diff / 60_000)
if (m < 1) return "just now"
if (m < 60) return `${m}m ago`
const h = Math.floor(m / 60)
if (h < 24) return `${h}h ago`
const d = Math.floor(h / 24)
return `${d}d ago`
}
function truncate(s: string, max = 90): string {
return s.length <= max ? s : s.slice(0, max - 1) + "…"
timestamp: number
meta?: Record<string, unknown>
}
export function TelegramThreadCard() {
const { data } = useSWR<TelegramResponse>(
"/api/chat?source=telegram&limit=5",
fetcher,
{ refreshInterval: 60_000, shouldRetryOnError: false }
)
const [messages, setMessages] = useState<TelegramMessage[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetch("/api/chat/history?limit=5")
.then(r => r.json())
.then(data => {
if (data?.messages) {
setMessages(data.messages.slice(-5).reverse())
}
setLoading(false)
})
.catch(e => {
console.error("Failed to load:", e)
setError(e.message)
setLoading(false)
})
}, [])
const messages = data?.messages ?? []
const hasData = messages.length > 0
// Simple timestamp formatter
const formatTime = (ts: number) => {
if (!ts) return ""
const diff = Date.now() - ts
const mins = Math.floor(diff / 60000)
if (mins < 1) return "just now"
if (mins < 60) return `${mins}m ago`
const hours = Math.floor(mins / 60)
if (hours < 24) return `${hours}h ago`
return new Date(ts).toLocaleDateString()
}
// Simple truncate
const truncate = (text: string, max = 40) => {
if (!text) return ""
return text.length > max ? text.slice(0, max) + "..." : text
}
if (loading) {
return (
<Card className="bg-card/40 p-4 flex flex-col">
<div className="flex items-center gap-2 mb-3">
<Send className="h-4 w-4 text-primary" />
<span className="text-[11px] uppercase tracking-wider text-muted-foreground/80">Telegram thread</span>
</div>
<div className="flex-1 flex items-center justify-center">
<span className="text-sm text-muted-foreground">Loading...</span>
</div>
</Card>
)
}
if (error) {
return (
<Card className="bg-card/40 p-4 flex flex-col">
<div className="flex items-center gap-2 mb-3">
<Send className="h-4 w-4 text-primary" />
<span className="text-[11px] uppercase tracking-wider text-muted-foreground/80">Telegram thread</span>
</div>
<div className="flex-1 flex items-center justify-center">
<span className="text-sm text-red-500">Error: {error}</span>
</div>
</Card>
)
}
return (
<Card className="bg-card/40 p-4 flex flex-col">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Send className="h-4 w-4 text-primary" />
<span className="text-[11px] uppercase tracking-wider text-muted-foreground/80">
Telegram thread
</span>
<span className="text-[11px] uppercase tracking-wider text-muted-foreground/80">Chat history</span>
</div>
<a href="/chat" className="text-xs text-primary hover:underline">
Open chat
</a>
<a href="/chat?session=telegram" className="text-xs text-primary hover:underline">Open chat </a>
</div>
{hasData ? (
@ -77,24 +99,21 @@ export function TelegramThreadCard() {
{msg.role === "user" ? "You" : "Tiger"}
</span>
<span className="text-[10px] text-muted-foreground">
{relTime(msg.ts)}
{formatTime(msg.timestamp)}
</span>
</div>
<p className="text-foreground/85 text-xs leading-relaxed mt-0.5">
<div className="text-xs text-muted-foreground/80 truncate">
{truncate(msg.content)}
</p>
</div>
</li>
))}
</ul>
) : (
<div className="flex-1 flex flex-col items-center justify-center text-center py-6 px-2">
<Send className="h-8 w-8 text-muted-foreground/30 mb-2" />
<p className="text-sm text-muted-foreground">
No Telegram messages mirrored yet.
</p>
<p className="text-sm text-muted-foreground">No messages yet.</p>
<p className="text-[11px] text-muted-foreground/60 mt-1 max-w-[260px]">
Phase 4 will sync your @Tiger_4321_bot conversation here so web
and Telegram share one history.
Start a conversation to see messages here.
</p>
</div>
)}

View file

@ -131,11 +131,16 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
// Initial mount: pick stored sessionKey, load its history, fetch sessions list.
React.useEffect(() => {
let cancelled = false
// Check URL for session param
const urlParams = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '')
const sessionParam = urlParams.get('session')
let initialKey = sessionParam ? `agent:main:${sessionParam}` : readPersistedKey()
async function init() {
const persisted = readPersistedKey()
if (cancelled) return
setCurrentSessionKey(persisted)
const [hist] = await Promise.all([loadHistoryFor(persisted), refreshSessions()])
setCurrentSessionKey(initialKey)
const [hist] = await Promise.all([loadHistoryFor(initialKey), refreshSessions()])
if (!cancelled) {
setMessages(hist)
setLoading(false)