mirror of
https://github.com/jcreek/AutomatedAssessmentFeedbackAgent.git
synced 2026-07-13 02:53:49 +00:00
feat(*): Improve the display of tool usage
This commit is contained in:
@@ -1,7 +1,13 @@
|
|||||||
import { ToolUtility } from '@azure/ai-projects';
|
import { ToolUtility } from '@azure/ai-projects';
|
||||||
|
|
||||||
|
export const conceptVerifierToolMeta = {
|
||||||
|
key: 'verifyConceptCoverage',
|
||||||
|
userDescription: 'Checking for required concepts from the rubric',
|
||||||
|
icon: '📋',
|
||||||
|
};
|
||||||
|
|
||||||
export const conceptVerifierTool = ToolUtility.createFunctionTool({
|
export const conceptVerifierTool = ToolUtility.createFunctionTool({
|
||||||
name: 'verifyConceptCoverage',
|
name: conceptVerifierToolMeta.key,
|
||||||
description: 'Checks which rubric‑listed concepts appear (or are missing) in the submission.',
|
description: 'Checks which rubric‑listed concepts appear (or are missing) in the submission.',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { ToolUtility } from '@azure/ai-projects';
|
import { ToolUtility } from '@azure/ai-projects';
|
||||||
|
|
||||||
|
export const essayAnalyzerToolMeta = {
|
||||||
|
key: 'analyzeEssay',
|
||||||
|
userDescription: 'Analyzing essay structure and argument quality',
|
||||||
|
icon: '📝',
|
||||||
|
};
|
||||||
|
|
||||||
export const essayAnalyzerTool = ToolUtility.createFunctionTool({
|
export const essayAnalyzerTool = ToolUtility.createFunctionTool({
|
||||||
name: 'analyzeEssay',
|
name: essayAnalyzerToolMeta.key,
|
||||||
description: 'Analyzes an essay for structure, argument, and evidence.',
|
description: 'Analyzes an essay for structure, argument, and evidence.',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { ToolUtility } from '@azure/ai-projects';
|
import { ToolUtility } from '@azure/ai-projects';
|
||||||
|
|
||||||
|
export const feedbackGeneratorToolMeta = {
|
||||||
|
key: 'generateFeedback',
|
||||||
|
userDescription: 'Generating personalized feedback for the student',
|
||||||
|
icon: '🌟',
|
||||||
|
};
|
||||||
|
|
||||||
export const feedbackGeneratorTool = ToolUtility.createFunctionTool({
|
export const feedbackGeneratorTool = ToolUtility.createFunctionTool({
|
||||||
name: 'generateFeedback',
|
name: feedbackGeneratorToolMeta.key,
|
||||||
description: 'Produces two praise points and two actionable “try next time” suggestions based on submission and rubric.',
|
description: 'Produces two praise points and two actionable “try next time” suggestions based on submission and rubric.',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export { rubricMatcherTool } from './rubricMatcherTool';
|
export { rubricMatcherTool, rubricMatcherToolMeta } from './rubricMatcherTool';
|
||||||
export { essayAnalyzerTool } from './essayAnalyzerTool';
|
export { essayAnalyzerTool, essayAnalyzerToolMeta } from './essayAnalyzerTool';
|
||||||
export { conceptVerifierTool } from './conceptVerifierTool';
|
export { conceptVerifierTool, conceptVerifierToolMeta } from './conceptVerifierTool';
|
||||||
export { feedbackGeneratorTool } from './feedbackGeneratorTool';
|
export { feedbackGeneratorTool, feedbackGeneratorToolMeta } from './feedbackGeneratorTool';
|
||||||
export { metacognitiveReflectionPromptTool } from './metacognitiveReflectionPromptTool';
|
export { metacognitiveReflectionPromptTool, metacognitiveReflectionPromptToolMeta } from './metacognitiveReflectionPromptTool';
|
||||||
export { selfAssessmentTool } from './selfAssessmentTool';
|
export { selfAssessmentTool, selfAssessmentToolMeta } from './selfAssessmentTool';
|
||||||
export { spellingAndGrammarCheckerTool } from './spellingAndGrammarCheckerTool';
|
export { spellingAndGrammarCheckerTool, spellingAndGrammarCheckerToolMeta } from './spellingAndGrammarCheckerTool';
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { ToolUtility } from '@azure/ai-projects';
|
import { ToolUtility } from '@azure/ai-projects';
|
||||||
|
|
||||||
|
export const metacognitiveReflectionPromptToolMeta = {
|
||||||
|
key: 'generateReflectionPrompts',
|
||||||
|
userDescription: 'Suggesting ways the student can improve next time',
|
||||||
|
icon: '💭',
|
||||||
|
};
|
||||||
|
|
||||||
export const metacognitiveReflectionPromptTool = ToolUtility.createFunctionTool({
|
export const metacognitiveReflectionPromptTool = ToolUtility.createFunctionTool({
|
||||||
name: 'generateReflectionPrompts',
|
name: metacognitiveReflectionPromptToolMeta.key,
|
||||||
description: 'Based on rubric shortfalls, generates prompts like “How could you strengthen your evidence in paragraph 2?”',
|
description: 'Based on rubric shortfalls, generates prompts like “How could you strengthen your evidence in paragraph 2?”',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { ToolUtility } from '@azure/ai-projects';
|
import { ToolUtility } from '@azure/ai-projects';
|
||||||
|
|
||||||
|
export const rubricMatcherToolMeta = {
|
||||||
|
key: 'matchRubric',
|
||||||
|
userDescription: 'Assessing the submission against the provided rubric',
|
||||||
|
icon: '📊',
|
||||||
|
};
|
||||||
|
|
||||||
export const rubricMatcherTool = ToolUtility.createFunctionTool({
|
export const rubricMatcherTool = ToolUtility.createFunctionTool({
|
||||||
name: 'matchRubric',
|
name: rubricMatcherToolMeta.key,
|
||||||
description: 'Matches a submission to a rubric and returns a score/criteria breakdown.',
|
description: 'Matches a submission to a rubric and returns a score/criteria breakdown.',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { ToolUtility } from '@azure/ai-projects';
|
import { ToolUtility } from '@azure/ai-projects';
|
||||||
|
|
||||||
|
export const selfAssessmentToolMeta = {
|
||||||
|
key: 'createSelfAssessment',
|
||||||
|
userDescription: 'Creating self-reflection questions for the student',
|
||||||
|
icon: '🔎',
|
||||||
|
};
|
||||||
|
|
||||||
export const selfAssessmentTool = ToolUtility.createFunctionTool({
|
export const selfAssessmentTool = ToolUtility.createFunctionTool({
|
||||||
name: 'createSelfAssessment',
|
name: selfAssessmentToolMeta.key,
|
||||||
description: 'Generates 3 short questions that prompt the student to reflect on their own work based on rubric gaps.',
|
description: 'Generates 3 short questions that prompt the student to reflect on their own work based on rubric gaps.',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { ToolUtility } from '@azure/ai-projects';
|
import { ToolUtility } from '@azure/ai-projects';
|
||||||
|
|
||||||
|
export const spellingAndGrammarCheckerToolMeta = {
|
||||||
|
key: 'checkSpellingAndGrammar',
|
||||||
|
userDescription: 'Checking for spelling and grammar mistakes',
|
||||||
|
icon: '📝',
|
||||||
|
};
|
||||||
|
|
||||||
export const spellingAndGrammarCheckerTool = ToolUtility.createFunctionTool({
|
export const spellingAndGrammarCheckerTool = ToolUtility.createFunctionTool({
|
||||||
name: 'checkSpellingAndGrammar',
|
name: spellingAndGrammarCheckerToolMeta.key,
|
||||||
description: 'Identifies spelling, punctuation, and syntax issues in the submission and suggests corrections.',
|
description: 'Identifies spelling, punctuation, and syntax issues in the submission and suggests corrections.',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
rubricMatcherToolMeta,
|
||||||
|
essayAnalyzerToolMeta,
|
||||||
|
conceptVerifierToolMeta,
|
||||||
|
feedbackGeneratorToolMeta,
|
||||||
|
metacognitiveReflectionPromptToolMeta,
|
||||||
|
selfAssessmentToolMeta,
|
||||||
|
spellingAndGrammarCheckerToolMeta
|
||||||
|
} from '$lib/agent/tools';
|
||||||
|
|
||||||
|
export let toolEvents: Array<{ tool: string, time: string }> = [];
|
||||||
|
|
||||||
|
// Map keys to meta objects for lookup
|
||||||
|
const TOOL_META_MAP: Record<string, { userDescription: string; icon: string }> = {
|
||||||
|
[rubricMatcherToolMeta.key]: rubricMatcherToolMeta,
|
||||||
|
[essayAnalyzerToolMeta.key]: essayAnalyzerToolMeta,
|
||||||
|
[conceptVerifierToolMeta.key]: conceptVerifierToolMeta,
|
||||||
|
[feedbackGeneratorToolMeta.key]: feedbackGeneratorToolMeta,
|
||||||
|
[metacognitiveReflectionPromptToolMeta.key]: metacognitiveReflectionPromptToolMeta,
|
||||||
|
[selfAssessmentToolMeta.key]: selfAssessmentToolMeta,
|
||||||
|
[spellingAndGrammarCheckerToolMeta.key]: spellingAndGrammarCheckerToolMeta
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build steps from toolEvents, filling in status, icon, and userDescription
|
||||||
|
$: steps = toolEvents.map((event, idx) => {
|
||||||
|
const meta = TOOL_META_MAP[event.tool];
|
||||||
|
return {
|
||||||
|
description: meta ? meta.userDescription : event.tool,
|
||||||
|
icon: meta ? meta.icon : '🤖',
|
||||||
|
status: idx < toolEvents.length - 1 ? 'done' : 'running',
|
||||||
|
time: event.time
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// If no events yet, show agent 'thinking'
|
||||||
|
$: agentThinking = toolEvents.length === 0;
|
||||||
|
$: statusMessage = agentThinking
|
||||||
|
? 'The AI Agent is analyzing your submission and selecting the best tools...'
|
||||||
|
: 'The AI Agent is now processing your submission...';
|
||||||
|
$: liveMessage = agentThinking
|
||||||
|
? statusMessage
|
||||||
|
: steps.length > 0
|
||||||
|
? steps[steps.length - 1].description
|
||||||
|
: statusMessage;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.agent-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.agent-avatar {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.status-message {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.progress-bar-bg {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
height: 10px;
|
||||||
|
background: #e0e7ef;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #5b9df9 0%, #4ade80 100%);
|
||||||
|
transition: width 0.5s;
|
||||||
|
}
|
||||||
|
ul.steps {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
li.step {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
li.step[aria-current="step"] {
|
||||||
|
background: #e0f2fe;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.step-icon {
|
||||||
|
margin-right: 1rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
min-width: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.visually-hidden {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="agent-container" role="region" aria-label="Submission Processing">
|
||||||
|
<div class="agent-avatar" aria-hidden="true">🤖</div>
|
||||||
|
<div class="status-message" id="agent-status">{statusMessage}</div>
|
||||||
|
<div class="progress-bar-bg" aria-hidden="true">
|
||||||
|
<div
|
||||||
|
class="progress-bar-fill"
|
||||||
|
style="width: {steps.length ? ((steps.filter(s => s.status === 'done').length) / (steps.length)) * 100 : 0}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div aria-live="polite" class="visually-hidden">{liveMessage}</div>
|
||||||
|
<ul class="steps" aria-label="Processing steps">
|
||||||
|
{#if agentThinking}
|
||||||
|
<li class="step" aria-current="step">
|
||||||
|
<span class="step-icon" aria-hidden="true">💡</span>
|
||||||
|
<span>AI Agent is analyzing your submission…</span>
|
||||||
|
</li>
|
||||||
|
{:else}
|
||||||
|
{#each steps as step, idx}
|
||||||
|
<li class="step {step.status}">
|
||||||
|
<span class="step-icon" aria-hidden="true">{step.icon}</span>
|
||||||
|
<span class="step-desc">{step.description}</span>
|
||||||
|
<span class="step-time">{new Date(step.time).toLocaleTimeString()}
|
||||||
|
{step.status === 'done'
|
||||||
|
? '✅'
|
||||||
|
: step.status === 'running'
|
||||||
|
? '⏳'
|
||||||
|
: '🛠️'}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
+180
-175
@@ -1,186 +1,191 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import AgenticProgress from '$lib/components/AgenticProgress.svelte';
|
||||||
|
|
||||||
let file: File | null = null;
|
let file: File | null = null;
|
||||||
let textInput = '';
|
let textInput = '';
|
||||||
let taskDescription = '';
|
let taskDescription = '';
|
||||||
let submitting = false;
|
let submitting = false;
|
||||||
let successMsg = '';
|
let successMsg = '';
|
||||||
let errorMsg = '';
|
let errorMsg = '';
|
||||||
let toolEvents: Array<{ tool: string, time: string }> = [];
|
let toolEvents: Array<{ tool: string; time: string }> = [];
|
||||||
let roomId: string = '';
|
let roomId: string = '';
|
||||||
let ws: WebSocket | null = null;
|
let ws: WebSocket | null = null;
|
||||||
const PARTYKIT_BASE_URL = import.meta.env.VITE_PARTYKIT_BASE_URL;
|
const PARTYKIT_BASE_URL = import.meta.env.VITE_PARTYKIT_BASE_URL;
|
||||||
|
|
||||||
function handleFileChange(event: Event) {
|
function handleFileChange(event: Event) {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
if (target.files && target.files.length > 0) {
|
if (target.files && target.files.length > 0) {
|
||||||
file = target.files[0];
|
file = target.files[0];
|
||||||
textInput = '';
|
textInput = '';
|
||||||
} else {
|
} else {
|
||||||
file = null;
|
file = null;
|
||||||
}
|
}
|
||||||
successMsg = '';
|
successMsg = '';
|
||||||
errorMsg = '';
|
errorMsg = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTextChange(event: Event) {
|
function handleTextChange(event: Event) {
|
||||||
const target = event.target as HTMLTextAreaElement;
|
const target = event.target as HTMLTextAreaElement;
|
||||||
textInput = target.value;
|
textInput = target.value;
|
||||||
if (textInput.length > 0) file = null;
|
if (textInput.length > 0) file = null;
|
||||||
successMsg = '';
|
successMsg = '';
|
||||||
errorMsg = '';
|
errorMsg = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit(event: Event) {
|
async function handleSubmit(event: Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
submitting = true;
|
submitting = true;
|
||||||
successMsg = '';
|
successMsg = '';
|
||||||
errorMsg = '';
|
errorMsg = '';
|
||||||
toolEvents = [];
|
toolEvents = [];
|
||||||
|
|
||||||
// Generate a unique roomId for this submission
|
// Generate a unique roomId for this submission
|
||||||
roomId = crypto.randomUUID();
|
roomId = crypto.randomUUID();
|
||||||
// Connect to PartyKit for this session
|
// Connect to PartyKit for this session
|
||||||
if (ws) ws.close();
|
if (ws) ws.close();
|
||||||
ws = new WebSocket(`${PARTYKIT_BASE_URL}/party/tool-usage-server-${roomId}`);
|
ws = new WebSocket(`${PARTYKIT_BASE_URL}/party/tool-usage-server-${roomId}`);
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
toolEvents = [...toolEvents, data];
|
toolEvents = [...toolEvents, data];
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!taskDescription.trim()) {
|
if (!taskDescription.trim()) {
|
||||||
errorMsg = 'Please provide a description of the task or assignment.';
|
errorMsg = 'Please provide a description of the task or assignment.';
|
||||||
submitting = false;
|
submitting = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('task', taskDescription.trim());
|
formData.append('task', taskDescription.trim());
|
||||||
if (file) {
|
if (file) {
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
} else if (textInput.trim().length > 0) {
|
} else if (textInput.trim().length > 0) {
|
||||||
formData.append('text', textInput.trim());
|
formData.append('text', textInput.trim());
|
||||||
}
|
}
|
||||||
formData.append('roomId', roomId);
|
formData.append('roomId', roomId);
|
||||||
|
|
||||||
errorMsg = '';
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/grade', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
result = await response.json();
|
|
||||||
} catch {
|
|
||||||
errorMsg = 'Received an invalid response from the server.';
|
|
||||||
submitting = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (response.ok && result.success) {
|
|
||||||
// Save to localStorage history
|
|
||||||
try {
|
|
||||||
let prev = JSON.parse(localStorage.getItem('assessmentHistory') || '[]');
|
|
||||||
const entry = {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
...result,
|
|
||||||
submission: result.submission || textInput || '',
|
|
||||||
task: taskDescription,
|
|
||||||
};
|
|
||||||
prev.unshift(entry);
|
|
||||||
localStorage.setItem('assessmentHistory', JSON.stringify(prev.slice(0, 50)));
|
|
||||||
} catch {}
|
|
||||||
// Redirect to /results
|
|
||||||
window.location.assign('/results');
|
|
||||||
file = null;
|
|
||||||
textInput = '';
|
|
||||||
taskDescription = '';
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
if (result && result.error) {
|
|
||||||
errorMsg = `Sorry, something went wrong: ${result.error}`;
|
|
||||||
} else if (response.status === 413) {
|
|
||||||
errorMsg = 'The uploaded file is too large. Please upload a smaller file.';
|
|
||||||
} else if (response.status >= 500) {
|
|
||||||
errorMsg = 'The server encountered an error. Please try again later.';
|
|
||||||
} else {
|
|
||||||
errorMsg = 'An unknown error occurred. Please check your input and try again.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
errorMsg = 'Network or server error. Please check your connection and try again.';
|
|
||||||
} finally {
|
|
||||||
submitting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
errorMsg = '';
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/grade', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await response.json();
|
||||||
|
} catch {
|
||||||
|
errorMsg = 'Received an invalid response from the server.';
|
||||||
|
submitting = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
// Save to localStorage history
|
||||||
|
try {
|
||||||
|
let prev = JSON.parse(localStorage.getItem('assessmentHistory') || '[]');
|
||||||
|
const entry = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
...result,
|
||||||
|
submission: result.submission || textInput || '',
|
||||||
|
task: taskDescription
|
||||||
|
};
|
||||||
|
prev.unshift(entry);
|
||||||
|
localStorage.setItem('assessmentHistory', JSON.stringify(prev.slice(0, 50)));
|
||||||
|
} catch {}
|
||||||
|
// Redirect to /results
|
||||||
|
window.location.assign('/results');
|
||||||
|
file = null;
|
||||||
|
textInput = '';
|
||||||
|
taskDescription = '';
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (result && result.error) {
|
||||||
|
errorMsg = `Sorry, something went wrong: ${result.error}`;
|
||||||
|
} else if (response.status === 413) {
|
||||||
|
errorMsg = 'The uploaded file is too large. Please upload a smaller file.';
|
||||||
|
} else if (response.status >= 500) {
|
||||||
|
errorMsg = 'The server encountered an error. Please try again later.';
|
||||||
|
} else {
|
||||||
|
errorMsg = 'An unknown error occurred. Please check your input and try again.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorMsg = 'Network or server error. Please check your connection and try again.';
|
||||||
|
} finally {
|
||||||
|
submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="max-w-xl mx-auto py-12 relative">
|
<section class="relative mx-auto max-w-xl py-12">
|
||||||
<h1 class="text-2xl font-bold mb-4">Upload Student Submissions</h1>
|
<h1 class="mb-4 text-2xl font-bold">Upload Student Submissions</h1>
|
||||||
<p class="mb-6">Upload student assignments for instant AI-powered assessment and feedback.</p>
|
<p class="mb-6">Upload student assignments for instant AI-powered assessment and feedback.</p>
|
||||||
{#if errorMsg}
|
{#if errorMsg}
|
||||||
<div class="mb-4 p-3 border border-red-300 bg-red-50 text-red-700 rounded" aria-live="assertive" role="alert">
|
<div
|
||||||
<strong>Error:</strong> {errorMsg}
|
class="mb-4 rounded border border-red-300 bg-red-50 p-3 text-red-700"
|
||||||
</div>
|
aria-live="assertive"
|
||||||
{/if}
|
role="alert"
|
||||||
{#if submitting}
|
>
|
||||||
<div id="submitting-container" class="absolute inset-0 flex flex-col items-center justify-center bg-white bg-opacity-80 z-20" role="status" aria-busy="true" aria-live="polite" tabindex="-1">
|
<strong>Error:</strong>
|
||||||
<svg class="animate-spin h-10 w-10 text-blue-600 mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
{errorMsg}
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
</div>
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
|
{/if}
|
||||||
</svg>
|
{#if submitting}
|
||||||
<span class="sr-only">Loading...</span>
|
<AgenticProgress {toolEvents} />
|
||||||
<span class="text-blue-700 font-semibold">Grading in progress...</span>
|
{:else}
|
||||||
<div class="mt-4 w-full max-w-md">
|
<form class="space-y-6" on:submit|preventDefault={handleSubmit}>
|
||||||
{#each toolEvents as event, i}
|
<div class="mb-4">
|
||||||
<div class="flex items-center space-x-2 text-sm text-gray-700 bg-blue-50 rounded p-2 my-1">
|
<label class="mb-1 block font-semibold" for="task"
|
||||||
<span>🛠️ {event.tool}</span>
|
>Task/Assignment Description <span class="text-red-500">*</span></label
|
||||||
<span class="text-xs text-gray-400">{new Date(event.time).toLocaleTimeString()}</span>
|
>
|
||||||
</div>
|
<textarea
|
||||||
{/each}
|
id="task"
|
||||||
</div>
|
class="w-full rounded border p-2"
|
||||||
</div>
|
rows="2"
|
||||||
{/if}
|
bind:value={taskDescription}
|
||||||
<form class="space-y-6" on:submit|preventDefault={handleSubmit}>
|
required
|
||||||
<div class="mb-4">
|
aria-required="true"
|
||||||
<label class="block mb-1 font-semibold" for="task">Task/Assignment Description <span class="text-red-500">*</span></label>
|
aria-label="Task or Assignment Description"
|
||||||
<textarea id="task" class="w-full border rounded p-2" rows="2" bind:value={taskDescription} required aria-required="true" aria-label="Task or Assignment Description" disabled={submitting}></textarea>
|
disabled={submitting}
|
||||||
</div>
|
></textarea>
|
||||||
<div class="mb-4">
|
</div>
|
||||||
<label class="block font-semibold mb-1" for="file">Upload a file (PDF, DOCX, TXT):</label>
|
<div class="mb-4">
|
||||||
<input
|
<label class="mb-1 block font-semibold" for="file">Upload a file (PDF, DOCX, TXT):</label>
|
||||||
id="file"
|
<input
|
||||||
type="file"
|
id="file"
|
||||||
accept=".pdf,.doc,.docx,.txt"
|
type="file"
|
||||||
class="block w-full border p-2 rounded"
|
accept=".pdf,.doc,.docx,.txt"
|
||||||
on:change={handleFileChange}
|
class="block w-full rounded border p-2"
|
||||||
disabled={submitting}
|
on:change={handleFileChange}
|
||||||
/>
|
disabled={submitting}
|
||||||
<span class="text-gray-500 text-sm">Or paste the student's submission below.</span>
|
/>
|
||||||
</div>
|
<span class="text-sm text-gray-500">Or paste the student's submission below.</span>
|
||||||
<div class="mb-4">
|
</div>
|
||||||
<label class="block font-semibold mb-1" for="textInput">Paste assignment text:</label>
|
<div class="mb-4">
|
||||||
<textarea
|
<label class="mb-1 block font-semibold" for="textInput">Paste assignment text:</label>
|
||||||
id="textInput"
|
<textarea
|
||||||
rows="5"
|
id="textInput"
|
||||||
class="w-full border p-2 rounded"
|
rows="5"
|
||||||
bind:value={textInput}
|
class="w-full rounded border p-2"
|
||||||
on:input={handleTextChange}
|
bind:value={textInput}
|
||||||
disabled={submitting}
|
on:input={handleTextChange}
|
||||||
placeholder="Paste or type student answer here..."
|
disabled={submitting}
|
||||||
></textarea>
|
placeholder="Paste or type student answer here..."
|
||||||
</div>
|
></textarea>
|
||||||
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 w-full mt-4" disabled={submitting || (!file && textInput.trim().length === 0)}>
|
</div>
|
||||||
{submitting ? 'Uploading...' : 'Submit'}
|
<button
|
||||||
</button>
|
type="submit"
|
||||||
{#if errorMsg}
|
class="mt-4 w-full rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||||||
<div class="mt-4 text-red-600">{errorMsg}</div>
|
disabled={submitting || (!file && textInput.trim().length === 0)}
|
||||||
{/if}
|
>
|
||||||
{#if successMsg}
|
{submitting ? 'Uploading...' : 'Submit'}
|
||||||
<div class="mt-4 text-green-600">{successMsg}</div>
|
</button>
|
||||||
{/if}
|
{#if errorMsg}
|
||||||
</form>
|
<div class="mt-4 text-red-600">{errorMsg}</div>
|
||||||
|
{/if}
|
||||||
|
{#if successMsg}
|
||||||
|
<div class="mt-4 text-green-600">{successMsg}</div>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user