feat: implement full context-scoped AI chat UI in AIChatPanel

- Added AIChatPanel component with context header, user and AI message handling.
- Integrated streaming responses via IPC and error handling for chat mutations.
- Enhanced user experience with input handling and auto-scrolling features.
- Updated AppShell to derive AI chat context from the current route.
- Introduced ScrollArea component for better scrolling behavior in various dialogs.
- Added support for Tailwind typography and improved global styles.
- Updated project and task dialogs to utilize ScrollArea for better UX.
This commit is contained in:
Roberto Musso
2026-02-24 12:02:06 +01:00
parent 00a43e0fbc
commit 5eb19e022e
20 changed files with 962 additions and 91 deletions

View File

@@ -6,7 +6,6 @@
*/
import { Annotation, StateGraph, START, END } from '@langchain/langgraph';
import { SystemMessage, HumanMessage, type BaseMessage } from '@langchain/core/messages';
import { z } from 'zod';
import { eq, asc } from 'drizzle-orm';
import { getDb } from '../db';
import { projects, tasks, checkpoints, notes, clients } from '../db/schema';
@@ -178,12 +177,6 @@ If the user asks about specific note contents that aren't included here, let the
// LangGraph State
// ---------------------------------------------------------------------------
const RouteSchema = z.object({
route: z.enum(['project', 'knowledge', 'general']).describe(
'Which specialist agent should handle this request',
),
});
const OrchestratorState = Annotation.Root({
/** The user's original message */
userMessage: Annotation<string>(),
@@ -207,25 +200,29 @@ type State = typeof OrchestratorState.State;
// Graph nodes
// ---------------------------------------------------------------------------
/** Node 1: Classify intent using structured output */
/** Node 1: Classify intent using plain-text extraction (works with all providers) */
async function classifyIntent(state: State): Promise<Partial<State>> {
const llm = await getLLM();
if (!llm) throw new Error('AI provider not configured. Please add your token in Settings.');
const routerLLM = llm.withStructuredOutput(RouteSchema);
const result = await routerLLM.invoke([
const response = await llm.invoke([
new SystemMessage(
`You are a routing classifier for Adiuva, a project management workspace.
Classify the user's message into one of these categories:
- "project": Question about a specific project (tasks, notes, checkpoints, progress, summaries)
- "knowledge": Cross-project or historical question (e.g., "what did we decide about X?", "find notes about Y")
- "general": Everything else (general help, scheduling, task overviews, workspace summaries)`,
Classify the user's message into exactly one category. Reply with ONLY the category name, nothing else.
Categories:
- project: Question about a specific project (tasks, notes, checkpoints, progress, summaries)
- knowledge: Cross-project or historical question (e.g., "what did we decide about X?", "find notes about Y")
- general: Everything else (general help, scheduling, task overviews, workspace summaries)`,
),
new HumanMessage(state.userMessage),
]);
return { route: result.route };
const text = (typeof response.content === 'string' ? response.content : '').trim().toLowerCase();
const validRoutes = ['project', 'knowledge', 'general'] as const;
const route = validRoutes.find((r) => text.includes(r)) ?? 'general';
return { route };
}
/** Node 2a: Project agent — answer project-scoped questions */