feat(ai-assistant): odt support, document-to-lesson attach, upload UX fixes
- Add .odt (application/vnd.oasis.opendocument.text) as a supported upload format; extract text via JSZip + content.xml parsing - Add attach_document_to_lesson agent tool so the AI can attach an uploaded document to a lesson as a downloadable material - Instruct the agent to create a "Course Materials" lesson (first lesson of the first section) and attach the uploaded document to it on implementation - Suppress hallucinated references: uploaded documents do not count as fetched URLs — omit the References section when no fetch_documentation_url calls were made - Surface specific upload error snackbars (file_too_large, unsupported_file_type) instead of silently logging; fix generic fallback message - Allow typing in the chat textarea while a document is uploading; only block Enter/send, not the whole input - Fix retry endpoint to return the current run instead of erroring when the run is already active (queued/running/waiting_for_input/paused) - Connect @cio/core Redis singleton in the jobs bootstrap so course-generation workers no longer fail with "The client is closed" - Expose documentAssets (assetId + fileName) alongside documentText in AgentContext and pass it through both the chat route and course runner
This commit is contained in:
@@ -279,6 +279,8 @@ If this conversation contains any successful \`fetch_documentation_url\` tool re
|
||||
|
||||
When any \`fetch_documentation_url\` results exist in this conversation, **every** lesson you create or update via \`update_lesson_content\` MUST end with a References section so the teacher can verify your sourcing. Skip the References section ONLY when zero docs have been fetched in the entire conversation.
|
||||
|
||||
**Uploaded documents do NOT count as fetched documentation for the purposes of References.** If the only source material is an uploaded document (no \`fetch_documentation_url\` calls were made), omit the References section entirely — do not fabricate URLs or cite the document by a made-up link.
|
||||
|
||||
Format the section as the last block of the lesson HTML, in this exact shape:
|
||||
|
||||
\`\`\`html
|
||||
@@ -431,11 +433,30 @@ export function buildTeacherContextMessage(
|
||||
contextLines.push(exerciseInfo);
|
||||
}
|
||||
if (context.documentText) {
|
||||
const docAssets = context.documentAssets ?? [];
|
||||
const assetLines = docAssets
|
||||
.filter((d) => d.assetId)
|
||||
.map((d) => `- documentId: ${d.documentId}, fileName: ${d.fileName}`)
|
||||
.join('\n');
|
||||
|
||||
const courseMaterialsInstruction =
|
||||
assetLines.length > 0
|
||||
? `\n\nWhen implementing a course plan, create a lesson titled **"Course Materials"** as the **first lesson of the first section**. Give it no text content. Then immediately call \`attach_document_to_lesson\` for each document listed below to attach the original file to that lesson:\n${assetLines}`
|
||||
: '';
|
||||
|
||||
contextLines.push(
|
||||
`The teacher has uploaded a document. Use this content as the source material for course planning and content generation:\n\n<document>\n${context.documentText}\n</document>`
|
||||
`The teacher has uploaded a document. Use this content as the source material for course planning and content generation:${courseMaterialsInstruction}\n\n<document>\n${context.documentText}\n</document>`
|
||||
);
|
||||
}
|
||||
if (context.existingSectionCount && context.existingSectionCount > 0) {
|
||||
if (context.isContentGroupingEnabled === false) {
|
||||
contextLines.push(
|
||||
`**Content grouping is DISABLED for this course.** Lessons and exercises are displayed as a flat ordered list — sections are invisible to students and teachers. Follow these rules strictly:\n` +
|
||||
`- Do NOT plan or create multiple sections. Use exactly ONE section as a technical container for all content.\n` +
|
||||
`- In \`generate_course_plan\`, output a single section whose title matches the course title. Place all lessons and exercises as items inside that one section, ordered sequentially (order 0, 1, 2, …).\n` +
|
||||
`- When implementing a plan, create that one section first, then add every lesson and exercise into it with strictly ascending \`order\` values (0, 1, 2, …). The order field is the only thing that controls the visible sequence.\n` +
|
||||
`- Never call \`create_section\` more than once for this course.`
|
||||
);
|
||||
} else if (context.existingSectionCount && context.existingSectionCount > 0) {
|
||||
contextLines.push(
|
||||
`This course already has ${context.existingSectionCount} sections. When creating new sections, set their order values starting after the existing sections.`
|
||||
);
|
||||
|
||||
@@ -43,7 +43,11 @@ export interface AgentContext {
|
||||
exerciseTitle?: string;
|
||||
documentId?: string;
|
||||
documentText?: string;
|
||||
/** Metadata for documents attached to this conversation — used to attach originals to lessons. */
|
||||
documentAssets?: { documentId: string; assetId: string | null; fileName: string }[];
|
||||
existingSectionCount?: number;
|
||||
/** When false, content grouping is disabled: lessons/exercises are displayed as a flat ordered list and sections are hidden from students. */
|
||||
isContentGroupingEnabled?: boolean;
|
||||
}
|
||||
|
||||
// ─── Course Plan (generated by the agent in Plan Mode) ───────────────────────
|
||||
@@ -215,11 +219,12 @@ export const MAX_STEPS_PER_ROUND = 15;
|
||||
export const SUPPORTED_DOCUMENT_TYPES = [
|
||||
'application/pdf',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/vnd.oasis.opendocument.text'
|
||||
] as const;
|
||||
|
||||
/** Supported file extensions for document upload */
|
||||
export const SUPPORTED_DOCUMENT_EXTENSIONS = ['.pdf', '.docx', '.pptx'] as const;
|
||||
export const SUPPORTED_DOCUMENT_EXTENSIONS = ['.pdf', '.docx', '.pptx', '.odt'] as const;
|
||||
|
||||
/** Token cost estimation heuristics (tokens per item) */
|
||||
export const TOKEN_COST_ESTIMATES = {
|
||||
|
||||
Reference in New Issue
Block a user