feat(dashboard): chat-context streaming, telegram thread card, digest/sidebar tweaks, file-tasks route
This commit is contained in:
parent
84aa98dd14
commit
0970160f29
6 changed files with 110 additions and 64 deletions
|
|
@ -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) {
|
export async function POST(request: NextRequest) {
|
||||||
const body = await request.json().catch(() => ({}));
|
const body = await request.json().catch(() => ({}));
|
||||||
const message: string = body?.message;
|
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
|
// Persist user message NOW, before the LLM call
|
||||||
// history still records what the user said.
|
|
||||||
await persistMessage("user", message, sessionKey);
|
await persistMessage("user", message, sessionKey);
|
||||||
|
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,17 @@ export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const section = searchParams.get("section"); // active | completed | all
|
const section = searchParams.get("section"); // active | completed | all
|
||||||
|
const project = searchParams.get("project"); // dashboard, oil, etc
|
||||||
|
|
||||||
let endpoint = "/tiger/file-tasks";
|
let endpoint = "/tiger/file-tasks";
|
||||||
if (section === "active") endpoint = "/tiger/file-tasks/active";
|
if (section === "active") endpoint = "/tiger/file-tasks/active";
|
||||||
else if (section === "completed") endpoint = "/tiger/file-tasks/completed";
|
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);
|
const result = await bridgeGet(endpoint);
|
||||||
return NextResponse.json(result);
|
return NextResponse.json(result);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ const navMain = [
|
||||||
{ title: "Agents", url: "/agents", icon: Bot },
|
{ title: "Agents", url: "/agents", icon: Bot },
|
||||||
{ title: "Knowledge", url: "/knowledge", icon: Brain },
|
{ title: "Knowledge", url: "/knowledge", icon: Brain },
|
||||||
{ title: "Workspace", url: "/workspace", icon: FolderOpen },
|
{ title: "Workspace", url: "/workspace", icon: FolderOpen },
|
||||||
|
{ title: "Activity", url: "/activity", icon: ScrollText },
|
||||||
{ title: "Cost", url: "/cost", icon: DollarSign },
|
{ title: "Cost", url: "/cost", icon: DollarSign },
|
||||||
{ title: "Logs", url: "/logs", icon: ScrollText },
|
{ title: "Logs", url: "/logs", icon: ScrollText },
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { bridgeGet } from "@/lib/bridge"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* digest-card.tsx — Today's digest of agent activity
|
* digest-card.tsx — Today's digest of agent activity
|
||||||
*
|
*
|
||||||
|
|
@ -27,7 +29,7 @@ interface ActivityResponse {
|
||||||
events: ActivityEvent[]
|
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 {
|
function relTime(ts: number): string {
|
||||||
if (!ts) return ""
|
if (!ts) return ""
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,93 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
/**
|
import { useEffect, useState } from "react"
|
||||||
* 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 { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
import { Send } from "lucide-react"
|
import { Send } from "lucide-react"
|
||||||
|
|
||||||
interface TelegramMessage {
|
interface TelegramMessage {
|
||||||
role: "user" | "agent" | "system"
|
role: string
|
||||||
content: string
|
content: string
|
||||||
ts: number
|
timestamp: number
|
||||||
}
|
meta?: Record<string, unknown>
|
||||||
|
|
||||||
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) + "…"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TelegramThreadCard() {
|
export function TelegramThreadCard() {
|
||||||
const { data } = useSWR<TelegramResponse>(
|
const [messages, setMessages] = useState<TelegramMessage[]>([])
|
||||||
"/api/chat?source=telegram&limit=5",
|
const [loading, setLoading] = useState(true)
|
||||||
fetcher,
|
const [error, setError] = useState<string | null>(null)
|
||||||
{ refreshInterval: 60_000, shouldRetryOnError: false }
|
|
||||||
)
|
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
|
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 (
|
return (
|
||||||
<Card className="bg-card/40 p-4 flex flex-col">
|
<Card className="bg-card/40 p-4 flex flex-col">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Send className="h-4 w-4 text-primary" />
|
<Send className="h-4 w-4 text-primary" />
|
||||||
<span className="text-[11px] uppercase tracking-wider text-muted-foreground/80">
|
<span className="text-[11px] uppercase tracking-wider text-muted-foreground/80">Chat history</span>
|
||||||
Telegram thread
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<a href="/chat" className="text-xs text-primary hover:underline">
|
<a href="/chat?session=telegram" className="text-xs text-primary hover:underline">Open chat →</a>
|
||||||
Open chat →
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasData ? (
|
{hasData ? (
|
||||||
|
|
@ -77,24 +99,21 @@ export function TelegramThreadCard() {
|
||||||
{msg.role === "user" ? "You" : "Tiger"}
|
{msg.role === "user" ? "You" : "Tiger"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-[10px] text-muted-foreground">
|
||||||
{relTime(msg.ts)}
|
{formatTime(msg.timestamp)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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)}
|
{truncate(msg.content)}
|
||||||
</p>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex flex-col items-center justify-center text-center py-6 px-2">
|
<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" />
|
<Send className="h-8 w-8 text-muted-foreground/30 mb-2" />
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">No messages yet.</p>
|
||||||
No Telegram messages mirrored yet.
|
|
||||||
</p>
|
|
||||||
<p className="text-[11px] text-muted-foreground/60 mt-1 max-w-[260px]">
|
<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
|
Start a conversation to see messages here.
|
||||||
and Telegram share one history.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -131,11 +131,16 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
|
||||||
// Initial mount: pick stored sessionKey, load its history, fetch sessions list.
|
// Initial mount: pick stored sessionKey, load its history, fetch sessions list.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let cancelled = false
|
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() {
|
async function init() {
|
||||||
const persisted = readPersistedKey()
|
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
setCurrentSessionKey(persisted)
|
setCurrentSessionKey(initialKey)
|
||||||
const [hist] = await Promise.all([loadHistoryFor(persisted), refreshSessions()])
|
const [hist] = await Promise.all([loadHistoryFor(initialKey), refreshSessions()])
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setMessages(hist)
|
setMessages(hist)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue