feat(planning): ship holiday-aware planning and assistant upgrades
This commit is contained in:
@@ -38,6 +38,26 @@ function resolvePageContext(pathname: string): string {
|
||||
interface Message {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
insights?: AssistantInsight[];
|
||||
}
|
||||
|
||||
interface AssistantInsightMetric {
|
||||
label: string;
|
||||
value: string;
|
||||
tone?: "neutral" | "good" | "warn" | "danger" | "info";
|
||||
}
|
||||
|
||||
interface AssistantInsightSection {
|
||||
title: string;
|
||||
metrics: AssistantInsightMetric[];
|
||||
}
|
||||
|
||||
interface AssistantInsight {
|
||||
kind: "chargeability" | "resource_match" | "holiday_region" | "resource_holidays";
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
metrics: AssistantInsightMetric[];
|
||||
sections?: AssistantInsightSection[];
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "capakraken-chat-messages";
|
||||
@@ -47,7 +67,23 @@ function loadPersistedMessages(): Message[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
const raw = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw) as Message[];
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed
|
||||
.filter((item): item is Partial<Message> & { role: Message["role"]; content: string } => (
|
||||
typeof item === "object"
|
||||
&& item !== null
|
||||
&& (item.role === "user" || item.role === "assistant")
|
||||
&& typeof item.content === "string"
|
||||
))
|
||||
.map((item) => ({
|
||||
role: item.role,
|
||||
content: item.content,
|
||||
...(Array.isArray(item.insights) ? { insights: item.insights as AssistantInsight[] } : {}),
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch { /* ignore corrupt data */ }
|
||||
return [];
|
||||
}
|
||||
@@ -101,10 +137,23 @@ export function ChatPanel({ onClose }: { onClose: () => void }) {
|
||||
messages: updated.slice(-40).map((m) => ({ role: m.role, content: m.content })),
|
||||
...(pathname ? { pageContext: resolvePageContext(pathname) } : {}),
|
||||
});
|
||||
setMessages((prev) => [...prev, { role: "assistant", content: reply.content }]);
|
||||
const typedReply = reply as {
|
||||
content: string;
|
||||
role: "assistant";
|
||||
actions?: Array<{ type: string; url?: string; scope?: string[] }>;
|
||||
insights?: AssistantInsight[];
|
||||
};
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
role: "assistant",
|
||||
content: typedReply.content,
|
||||
...(Array.isArray(typedReply.insights) && typedReply.insights.length > 0 ? { insights: typedReply.insights } : {}),
|
||||
},
|
||||
]);
|
||||
|
||||
// Handle actions from the AI (navigation, data invalidation)
|
||||
const actions = (reply as { actions?: Array<{ type: string; url?: string; scope?: string[] }> }).actions;
|
||||
const actions = typedReply.actions;
|
||||
if (actions) {
|
||||
for (const action of actions) {
|
||||
if (action.type === "navigate" && action.url) {
|
||||
@@ -230,7 +279,12 @@ export function ChatPanel({ onClose }: { onClose: () => void }) {
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg, i) => (
|
||||
<ChatMessage key={i} role={msg.role} content={msg.content} />
|
||||
<ChatMessage
|
||||
key={i}
|
||||
role={msg.role}
|
||||
content={msg.content}
|
||||
{...(msg.insights ? { insights: msg.insights } : {})}
|
||||
/>
|
||||
))}
|
||||
{isLoading && <TypingIndicator />}
|
||||
{error && (
|
||||
|
||||
Reference in New Issue
Block a user