chore: auto compact chat
This commit is contained in:
@@ -51,7 +51,6 @@
|
||||
AI_ASSISTANT_QUICK_ACTION_ENTRIES,
|
||||
STUDENT_QUICK_ACTION_ENTRIES
|
||||
} from '$features/ai-assistant/utils/constants';
|
||||
import { calculateContextUsage } from '$features/ai-assistant/utils/context-utils';
|
||||
import {
|
||||
AGENT_MODELS,
|
||||
AGENT_MODEL_IDS,
|
||||
@@ -80,7 +79,6 @@
|
||||
let inputValue = $state('');
|
||||
let uploadedDocument: UploadedDocument | null = $state(null);
|
||||
let isUploading = $state(false);
|
||||
let contextFullBusy = $state<null | 'compact' | 'new_chat'>(null);
|
||||
|
||||
let pendingInitialTemplateId: CourseTemplateId | null = $state(null);
|
||||
|
||||
@@ -321,6 +319,7 @@
|
||||
credentials: 'include',
|
||||
body: () => ({
|
||||
courseId,
|
||||
conversationId: activeConversationId ?? undefined,
|
||||
model: selectedModel,
|
||||
context: {
|
||||
lessonId: page.params?.lessonId,
|
||||
@@ -379,8 +378,9 @@
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function handleSend() {
|
||||
async function handleSend() {
|
||||
if (!inputValue.trim() || chat.status === 'streaming') return;
|
||||
if (!courseId) return;
|
||||
|
||||
const text = inputValue;
|
||||
const userMessageCount = chat.messages.filter((message) => message.role === 'user').length;
|
||||
@@ -402,22 +402,17 @@
|
||||
if (templateForFirstMessage) {
|
||||
const templateMeta: AiAssistantTemplateMetadata = { id: templateForFirstMessage };
|
||||
metadata.template = templateMeta;
|
||||
}
|
||||
|
||||
const conversationId = await ensureActiveConversation(courseId);
|
||||
if (!conversationId) return;
|
||||
|
||||
if (templateForFirstMessage) {
|
||||
pendingInitialTemplateId = null;
|
||||
}
|
||||
|
||||
inputValue = '';
|
||||
|
||||
// Create conversation eagerly before sending the first message
|
||||
if (!activeConversationId) {
|
||||
if (!courseId) return;
|
||||
|
||||
aiAssistantApi.createConversation(courseId).then((created) => {
|
||||
if (created) {
|
||||
setActiveConversationId(courseId, created.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
chat.sendMessage({
|
||||
text,
|
||||
...(Object.keys(metadata).length > 0 ? { metadata } : {})
|
||||
@@ -433,7 +428,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureActiveConversation(courseId);
|
||||
const conversationId = await ensureActiveConversation(courseId);
|
||||
if (!conversationId) return;
|
||||
|
||||
chat.sendMessage({
|
||||
text: buildTemplateAnswersSummary(payload.templateId, payload.answers, payload.fields),
|
||||
@@ -452,7 +448,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureActiveConversation(courseId);
|
||||
const conversationId = await ensureActiveConversation(courseId);
|
||||
if (!conversationId) return;
|
||||
|
||||
chat.sendMessage({
|
||||
text: "I'll answer your questions in chat instead of the form.",
|
||||
@@ -499,28 +496,22 @@
|
||||
|
||||
function handleQuickAction(action: string) {
|
||||
inputValue = action;
|
||||
handleSend();
|
||||
void handleSend();
|
||||
}
|
||||
|
||||
function handleStop() {
|
||||
chat.stop();
|
||||
}
|
||||
|
||||
function handleImplementPlan(editedPlan: unknown) {
|
||||
async function handleImplementPlan(editedPlan: unknown) {
|
||||
if (chat.status === 'streaming') return;
|
||||
if (!courseId) return;
|
||||
|
||||
const conversationId = await ensureActiveConversation(courseId);
|
||||
if (!conversationId) return;
|
||||
|
||||
inputValue = '';
|
||||
|
||||
if (!activeConversationId) {
|
||||
if (!courseId) return;
|
||||
|
||||
aiAssistantApi.createConversation(courseId).then((created) => {
|
||||
if (created) {
|
||||
setActiveConversationId(courseId, created.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
chat.sendMessage({
|
||||
text: 'Implement this plan.',
|
||||
metadata: {
|
||||
@@ -532,74 +523,27 @@
|
||||
});
|
||||
}
|
||||
|
||||
function handleAskPlanChanges(message: string) {
|
||||
async function handleAskPlanChanges(message: string) {
|
||||
const trimmed = message.trim();
|
||||
|
||||
if (!trimmed || chat.status === 'streaming') return;
|
||||
if (!courseId) return;
|
||||
|
||||
if (!activeConversationId && courseId) {
|
||||
aiAssistantApi.createConversation(courseId).then((created) => {
|
||||
if (created) {
|
||||
setActiveConversationId(courseId, created.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
const conversationId = await ensureActiveConversation(courseId);
|
||||
if (!conversationId) return;
|
||||
|
||||
chat.sendMessage({ text: trimmed });
|
||||
}
|
||||
|
||||
function handleResume() {
|
||||
inputValue = CONTINUE_IMPLEMENTATION_PROMPT;
|
||||
handleSend();
|
||||
void handleSend();
|
||||
}
|
||||
|
||||
function handleMentionClick(route: string) {
|
||||
goto(resolve(route, {}));
|
||||
}
|
||||
|
||||
async function handleStartNewChatWithSummary() {
|
||||
if (!courseId || contextFullBusy) return;
|
||||
|
||||
contextFullBusy = 'new_chat';
|
||||
|
||||
try {
|
||||
const summary = await aiAssistantApi.summarizeConversation(chat.messages as AiAssistantMessage[], courseId);
|
||||
|
||||
chat.messages = [];
|
||||
setActiveConversationId(courseId, null);
|
||||
|
||||
const created = await aiAssistantApi.createConversation(courseId);
|
||||
|
||||
if (created) {
|
||||
setActiveConversationId(courseId, created.id);
|
||||
}
|
||||
|
||||
if (summary) {
|
||||
inputValue = summary;
|
||||
handleSend();
|
||||
}
|
||||
} finally {
|
||||
contextFullBusy = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCompactConversation() {
|
||||
if (!courseId || !activeConversationId || contextFullBusy) return;
|
||||
|
||||
contextFullBusy = 'compact';
|
||||
|
||||
try {
|
||||
const compacted = await aiAssistantApi.compactConversation(activeConversationId);
|
||||
|
||||
if (compacted && compacted.length > 0) {
|
||||
chat.messages = compacted as AiAssistantMessage[];
|
||||
await chat.regenerate();
|
||||
}
|
||||
} finally {
|
||||
contextFullBusy = null;
|
||||
}
|
||||
}
|
||||
|
||||
const isStreaming = $derived(chat.status === 'streaming' || chat.status === 'submitted');
|
||||
const isExhausted = $derived(tokenUsage !== null && tokenUsage.remaining <= 0);
|
||||
|
||||
@@ -641,10 +585,6 @@
|
||||
};
|
||||
});
|
||||
|
||||
const contextUsage = $derived(
|
||||
calculateContextUsage(chat.messages as AiAssistantMessage[], AGENT_MODELS[selectedModel].contextWindow)
|
||||
);
|
||||
|
||||
const mentionItems = $derived(
|
||||
getMentionableContent(courseApi.course).map((item) => ({
|
||||
id: item.id,
|
||||
@@ -829,7 +769,7 @@
|
||||
}
|
||||
|
||||
inputValue = prompt;
|
||||
handleSend();
|
||||
void handleSend();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -897,13 +837,9 @@
|
||||
{isStreaming}
|
||||
{isExhausted}
|
||||
{isUploading}
|
||||
{contextFullBusy}
|
||||
compactConversationDisabled={!activeConversationId}
|
||||
onCompactConversation={handleCompactConversation}
|
||||
{uploadedDocument}
|
||||
{mentionItems}
|
||||
{selectedModel}
|
||||
{contextUsage}
|
||||
{isStudent}
|
||||
{tutorBlocked}
|
||||
lockedModelIds={$isFreePlan ? paidModelIds : []}
|
||||
@@ -914,6 +850,5 @@
|
||||
onRemoveDocument={handleRemoveDocument}
|
||||
onSelectModel={handleSelectModel}
|
||||
onLockedModelSelect={handleLockedModelSelect}
|
||||
onStartNewChatWithSummary={handleStartNewChatWithSummary}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
import { ModelPicker } from '@cio/ui/custom/model-picker';
|
||||
import type { AgentModelId } from '@cio/utils/agent-models';
|
||||
import { AI_AGENT_RUNNING_WARNING_DISMISSED_KEY } from '$features/ai-assistant/utils/constants';
|
||||
import ContextIndicator from './context-indicator.svelte';
|
||||
import ContextFullState from './context-full-state.svelte';
|
||||
import type { ContextUsage } from './utils/context-utils';
|
||||
|
||||
interface UploadedDocument {
|
||||
id: string;
|
||||
@@ -32,14 +29,11 @@
|
||||
isStreaming: boolean;
|
||||
isExhausted: boolean;
|
||||
isUploading: boolean;
|
||||
contextFullBusy: null | 'compact' | 'new_chat';
|
||||
compactConversationDisabled?: boolean;
|
||||
error: Error | null | undefined;
|
||||
mentionItems: MentionItem[];
|
||||
uploadedDocument: UploadedDocument | null;
|
||||
selectedModel: AgentModelId;
|
||||
lockedModelIds?: readonly AgentModelId[];
|
||||
contextUsage: ContextUsage | null;
|
||||
isStudent?: boolean;
|
||||
/** Set to 'LEARNER_CAP_REACHED' | 'POOL_EXHAUSTED' | 'AI_TUTOR_DISABLED' to render the take-a-break empty state. */
|
||||
tutorBlocked?: 'LEARNER_CAP_REACHED' | 'POOL_EXHAUSTED' | 'AI_TUTOR_DISABLED' | null;
|
||||
@@ -49,8 +43,6 @@
|
||||
onRemoveDocument: () => void;
|
||||
onSelectModel: (id: AgentModelId) => void;
|
||||
onLockedModelSelect?: (id: AgentModelId) => void;
|
||||
onStartNewChatWithSummary: () => void;
|
||||
onCompactConversation: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -58,14 +50,11 @@
|
||||
isStreaming,
|
||||
isExhausted,
|
||||
isUploading,
|
||||
contextFullBusy,
|
||||
compactConversationDisabled = false,
|
||||
error,
|
||||
mentionItems,
|
||||
uploadedDocument,
|
||||
selectedModel,
|
||||
lockedModelIds = [],
|
||||
contextUsage,
|
||||
isStudent = false,
|
||||
tutorBlocked = null,
|
||||
onSend,
|
||||
@@ -73,9 +62,7 @@
|
||||
onFileSelect,
|
||||
onRemoveDocument,
|
||||
onSelectModel,
|
||||
onLockedModelSelect,
|
||||
onStartNewChatWithSummary,
|
||||
onCompactConversation
|
||||
onLockedModelSelect
|
||||
}: Props = $props();
|
||||
|
||||
function tutorBlockedMessage(reason: NonNullable<typeof tutorBlocked>): string {
|
||||
@@ -200,13 +187,6 @@
|
||||
<p class="text-xs">{tutorBlockedMessage(tutorBlocked)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if contextUsage?.isFull && !isStreaming}
|
||||
<ContextFullState
|
||||
{contextFullBusy}
|
||||
{compactConversationDisabled}
|
||||
{onCompactConversation}
|
||||
onStartNewChat={onStartNewChatWithSummary}
|
||||
/>
|
||||
{:else}
|
||||
<div class="border-t px-3 pt-3 pb-1.5">
|
||||
{#if isExhausted}
|
||||
@@ -307,10 +287,6 @@
|
||||
|
||||
<div class="flex-1"></div>
|
||||
|
||||
{#if contextUsage && contextUsage.usedTokens > 0}
|
||||
<ContextIndicator {contextUsage} />
|
||||
{/if}
|
||||
|
||||
{#if isStreaming}
|
||||
<Button size="icon" variant="outline" onclick={onStop} class="size-7 shrink-0">
|
||||
<SquareIcon size={12} />
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
{#each inlineParts as part, partIndex (partIndex)}
|
||||
{#if part.type === 'text'}
|
||||
<div
|
||||
class="ai-chat-prose prose prose-sm dark:prose-invert max-w-none {message.role === 'user' &&
|
||||
class="ai-chat-prose prose prose-sm dark:prose-invert max-w-none break-all {message.role === 'user' &&
|
||||
'ui:text-primary-foreground!'}"
|
||||
>
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
|
||||
@@ -8,7 +8,6 @@ export const getSessionData = async (cookies: Cookies): Promise<App.Locals | nul
|
||||
try {
|
||||
const cioCookies = getCioCookieString(cookies);
|
||||
|
||||
console.log('cioCookies', cioCookies);
|
||||
if (!cioCookies) return null;
|
||||
|
||||
const locals = await getThroughAuthClient(cioCookies);
|
||||
|
||||
Reference in New Issue
Block a user