Improve MCP tooling and draft exercise publishing
This commit is contained in:
@@ -2,6 +2,7 @@ import { AppError, ErrorCodes } from '@api/utils/errors';
|
|||||||
import { env } from '@api/config/env';
|
import { env } from '@api/config/env';
|
||||||
import { getDashboardBaseUrl } from '@api/config/dashboard-url';
|
import { getDashboardBaseUrl } from '@api/config/dashboard-url';
|
||||||
import { createCourse, updateCourse } from '@api/services/course/course';
|
import { createCourse, updateCourse } from '@api/services/course/course';
|
||||||
|
import { createExercise, getExercise, listExercises, replaceExerciseService } from '@api/services/exercise';
|
||||||
import { createCourseSection, listCourseSections, updateCourseSectionService } from '@api/services/course/section';
|
import { createCourseSection, listCourseSections, updateCourseSectionService } from '@api/services/course/section';
|
||||||
import { createLesson, listLessons, updateLessonService } from '@api/services/lesson';
|
import { createLesson, listLessons, updateLessonService } from '@api/services/lesson';
|
||||||
import { upsertLessonLanguageService, updateLessonLanguageService } from '@api/services/lesson-language';
|
import { upsertLessonLanguageService, updateLessonLanguageService } from '@api/services/lesson-language';
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
type TCourseImportDraftPayload,
|
type TCourseImportDraftPayload,
|
||||||
type TCourseImportDraftPublish,
|
type TCourseImportDraftPublish,
|
||||||
type TCourseImportDraftPublishToCourse,
|
type TCourseImportDraftPublishToCourse,
|
||||||
|
type TCourseImportDraftExercise,
|
||||||
type TCourseImportDraftUpdate,
|
type TCourseImportDraftUpdate,
|
||||||
ZCourseImportDraftPayload,
|
ZCourseImportDraftPayload,
|
||||||
ZCourseImportWarning
|
ZCourseImportWarning
|
||||||
@@ -36,6 +38,7 @@ import { eq } from 'drizzle-orm';
|
|||||||
type DraftSummary = {
|
type DraftSummary = {
|
||||||
sectionCount: number;
|
sectionCount: number;
|
||||||
lessonCount: number;
|
lessonCount: number;
|
||||||
|
exerciseCount: number;
|
||||||
localeCount: number;
|
localeCount: number;
|
||||||
warningCount: number;
|
warningCount: number;
|
||||||
};
|
};
|
||||||
@@ -52,22 +55,26 @@ type PublishDraftResult = {
|
|||||||
tagNames: string[];
|
tagNames: string[];
|
||||||
createdSections: number;
|
createdSections: number;
|
||||||
createdLessons: number;
|
createdLessons: number;
|
||||||
|
createdExercises: number;
|
||||||
localeCount: number;
|
localeCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PublishDraftToExistingCourseResult = PublishDraftResult & {
|
type PublishDraftToExistingCourseResult = PublishDraftResult & {
|
||||||
updatedSections: number;
|
updatedSections: number;
|
||||||
updatedLessons: number;
|
updatedLessons: number;
|
||||||
|
updatedExercises: number;
|
||||||
createdLessonLanguages: number;
|
createdLessonLanguages: number;
|
||||||
updatedLessonLanguages: number;
|
updatedLessonLanguages: number;
|
||||||
untouchedSections: number;
|
untouchedSections: number;
|
||||||
untouchedLessons: number;
|
untouchedLessons: number;
|
||||||
|
untouchedExercises: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildDraftSummary(draft: TCourseImportDraftPayload): DraftSummary {
|
function buildDraftSummary(draft: TCourseImportDraftPayload): DraftSummary {
|
||||||
return {
|
return {
|
||||||
sectionCount: draft.sections.length,
|
sectionCount: draft.sections.length,
|
||||||
lessonCount: draft.lessons.length,
|
lessonCount: draft.lessons.length,
|
||||||
|
exerciseCount: draft.exercises?.length ?? 0,
|
||||||
localeCount: new Set(draft.lessonLanguages.map((item) => item.locale)).size,
|
localeCount: new Set(draft.lessonLanguages.map((item) => item.locale)).size,
|
||||||
warningCount: draft.warnings.length
|
warningCount: draft.warnings.length
|
||||||
};
|
};
|
||||||
@@ -241,6 +248,80 @@ function getPrimaryLessonContent(
|
|||||||
return lessonLanguages.find((lessonLanguage) => lessonLanguage.lessonExternalId === lessonExternalId)?.content;
|
return lessonLanguages.find((lessonLanguage) => lessonLanguage.lessonExternalId === lessonExternalId)?.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildExercisePublishPayload(
|
||||||
|
exercise: TCourseImportDraftExercise,
|
||||||
|
lessonIdMap: Map<string, string>,
|
||||||
|
sectionIdMap: Map<string, string>
|
||||||
|
) {
|
||||||
|
const lessonId = exercise.lessonExternalId ? lessonIdMap.get(exercise.lessonExternalId) : undefined;
|
||||||
|
if (exercise.lessonExternalId && !lessonId) {
|
||||||
|
throw new AppError(`Exercise "${exercise.title}" references an unknown lesson`, ErrorCodes.VALIDATION_ERROR, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionId = exercise.sectionExternalId ? sectionIdMap.get(exercise.sectionExternalId) : undefined;
|
||||||
|
if (exercise.sectionExternalId && !sectionId) {
|
||||||
|
throw new AppError(`Exercise "${exercise.title}" references an unknown section`, ErrorCodes.VALIDATION_ERROR, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: exercise.title,
|
||||||
|
description: exercise.description,
|
||||||
|
lessonId: lessonId ?? '',
|
||||||
|
sectionId: sectionId ?? '',
|
||||||
|
order: exercise.order,
|
||||||
|
dueBy: exercise.dueBy,
|
||||||
|
questions: exercise.questions?.map((question) => ({
|
||||||
|
question: question.question,
|
||||||
|
questionTypeId: question.questionTypeId,
|
||||||
|
points: question.points,
|
||||||
|
order: question.order,
|
||||||
|
settings: question.settings,
|
||||||
|
options: question.options?.map((option) => ({
|
||||||
|
label: option.label,
|
||||||
|
isCorrect: option.isCorrect,
|
||||||
|
settings: option.settings
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildDraftExercisesSnapshot(courseId: string): Promise<TCourseImportDraftPayload['exercises']> {
|
||||||
|
const exercises = await listExercises(courseId);
|
||||||
|
if (exercises.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailedExercises = await Promise.all(exercises.map((exercise) => getExercise(exercise.id)));
|
||||||
|
|
||||||
|
return detailedExercises.map((exercise) => ({
|
||||||
|
externalId: exercise.id,
|
||||||
|
lessonExternalId: exercise.lessonId ?? undefined,
|
||||||
|
sectionExternalId: exercise.lessonId ? undefined : (exercise.sectionId ?? undefined),
|
||||||
|
title: exercise.title,
|
||||||
|
description: exercise.description ?? undefined,
|
||||||
|
order: exercise.order ?? undefined,
|
||||||
|
dueBy: exercise.dueBy ?? undefined,
|
||||||
|
questions:
|
||||||
|
exercise.questions && exercise.questions.length > 0
|
||||||
|
? exercise.questions.map((question) => ({
|
||||||
|
question: question.title,
|
||||||
|
questionTypeId: question.questionTypeId,
|
||||||
|
points: question.points,
|
||||||
|
order: question.order,
|
||||||
|
settings: question.settings,
|
||||||
|
options:
|
||||||
|
question.options.length > 0
|
||||||
|
? question.options.map((option) => ({
|
||||||
|
label: option.label ?? '',
|
||||||
|
isCorrect: option.isCorrect,
|
||||||
|
settings: option.settings
|
||||||
|
}))
|
||||||
|
: undefined
|
||||||
|
}))
|
||||||
|
: undefined
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async function buildCourseStructureSnapshot(orgId: string, courseId: string): Promise<CourseStructureSnapshot> {
|
async function buildCourseStructureSnapshot(orgId: string, courseId: string): Promise<CourseStructureSnapshot> {
|
||||||
const course = await getCourseRecord(orgId, courseId);
|
const course = await getCourseRecord(orgId, courseId);
|
||||||
const [sections, lessons, tags] = await Promise.all([
|
const [sections, lessons, tags] = await Promise.all([
|
||||||
@@ -354,6 +435,8 @@ async function buildCourseStructureSnapshot(orgId: string, courseId: string): Pr
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedExercises = await buildDraftExercisesSnapshot(courseId);
|
||||||
|
|
||||||
const draft = ZCourseImportDraftPayload.parse({
|
const draft = ZCourseImportDraftPayload.parse({
|
||||||
course: {
|
course: {
|
||||||
title: course.title,
|
title: course.title,
|
||||||
@@ -366,6 +449,7 @@ async function buildCourseStructureSnapshot(orgId: string, courseId: string): Pr
|
|||||||
sections: normalizedSections,
|
sections: normalizedSections,
|
||||||
lessons: normalizedLessons,
|
lessons: normalizedLessons,
|
||||||
lessonLanguages: normalizedLessonLanguages,
|
lessonLanguages: normalizedLessonLanguages,
|
||||||
|
exercises: normalizedExercises,
|
||||||
sourceReferences: [
|
sourceReferences: [
|
||||||
{
|
{
|
||||||
type: 'course',
|
type: 'course',
|
||||||
@@ -571,6 +655,7 @@ export async function publishCourseImportDraftService(
|
|||||||
tagNames: normalizeDraftTagNames(draft.tags),
|
tagNames: normalizeDraftTagNames(draft.tags),
|
||||||
createdSections: 0,
|
createdSections: 0,
|
||||||
createdLessons: 0,
|
createdLessons: 0,
|
||||||
|
createdExercises: 0,
|
||||||
localeCount: 0
|
localeCount: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -647,6 +732,15 @@ export async function publishCourseImportDraftService(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let createdExercises = 0;
|
||||||
|
for (const exercise of draft.exercises ?? []) {
|
||||||
|
await createExercise({
|
||||||
|
courseId,
|
||||||
|
...buildExercisePublishPayload(exercise, lessonIdMap, sectionIdMap)
|
||||||
|
});
|
||||||
|
createdExercises += 1;
|
||||||
|
}
|
||||||
|
|
||||||
const appliedTagNames = normalizeDraftTagNames(draft.tags);
|
const appliedTagNames = normalizeDraftTagNames(draft.tags);
|
||||||
if (appliedTagNames.length > 0) {
|
if (appliedTagNames.length > 0) {
|
||||||
await assignCourseTagsByName(orgId, courseId, {
|
await assignCourseTagsByName(orgId, courseId, {
|
||||||
@@ -674,6 +768,7 @@ export async function publishCourseImportDraftService(
|
|||||||
tagNames: appliedTagNames,
|
tagNames: appliedTagNames,
|
||||||
createdSections: sectionIdMap.size,
|
createdSections: sectionIdMap.size,
|
||||||
createdLessons: lessonIdMap.size,
|
createdLessons: lessonIdMap.size,
|
||||||
|
createdExercises,
|
||||||
localeCount: new Set(draft.lessonLanguages.map((item) => item.locale)).size
|
localeCount: new Set(draft.lessonLanguages.map((item) => item.locale)).size
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -718,8 +813,10 @@ export async function publishCourseImportDraftToExistingCourseService(
|
|||||||
listCourseSections(course.id),
|
listCourseSections(course.id),
|
||||||
listLessons(course.id)
|
listLessons(course.id)
|
||||||
]);
|
]);
|
||||||
|
const existingExercises = await listExercises(course.id);
|
||||||
const existingSectionIds = new Set(existingSections.map((section) => section.id));
|
const existingSectionIds = new Set(existingSections.map((section) => section.id));
|
||||||
const existingLessonIds = new Set(existingLessons.map((lesson) => lesson.id));
|
const existingLessonIds = new Set(existingLessons.map((lesson) => lesson.id));
|
||||||
|
const existingExerciseIds = new Set(existingExercises.map((exercise) => exercise.id));
|
||||||
const existingLessonLanguages = await getLessonLanguagesByLessonIds(existingLessons.map((lesson) => lesson.id));
|
const existingLessonLanguages = await getLessonLanguagesByLessonIds(existingLessons.map((lesson) => lesson.id));
|
||||||
|
|
||||||
const sectionIdMap = new Map<string, string>();
|
const sectionIdMap = new Map<string, string>();
|
||||||
@@ -816,6 +913,25 @@ export async function publishCourseImportDraftToExistingCourseService(
|
|||||||
createdLessonLanguages += 1;
|
createdLessonLanguages += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let createdExercises = 0;
|
||||||
|
let updatedExercises = 0;
|
||||||
|
|
||||||
|
for (const exercise of draft.exercises ?? []) {
|
||||||
|
const payload = buildExercisePublishPayload(exercise, lessonIdMap, sectionIdMap);
|
||||||
|
|
||||||
|
if (existingExerciseIds.has(exercise.externalId)) {
|
||||||
|
await replaceExerciseService(exercise.externalId, payload);
|
||||||
|
updatedExercises += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await createExercise({
|
||||||
|
courseId: course.id,
|
||||||
|
...payload
|
||||||
|
});
|
||||||
|
createdExercises += 1;
|
||||||
|
}
|
||||||
|
|
||||||
const appliedTagNames = normalizeDraftTagNames(draft.tags);
|
const appliedTagNames = normalizeDraftTagNames(draft.tags);
|
||||||
if (appliedTagNames.length > 0) {
|
if (appliedTagNames.length > 0) {
|
||||||
await assignCourseTagsByName(orgId, course.id, {
|
await assignCourseTagsByName(orgId, course.id, {
|
||||||
@@ -826,6 +942,9 @@ export async function publishCourseImportDraftToExistingCourseService(
|
|||||||
|
|
||||||
const referencedSectionIds = new Set([...sectionIdMap.values()].filter((id) => existingSectionIds.has(id)));
|
const referencedSectionIds = new Set([...sectionIdMap.values()].filter((id) => existingSectionIds.has(id)));
|
||||||
const referencedLessonIds = new Set([...lessonIdMap.values()].filter((id) => existingLessonIds.has(id)));
|
const referencedLessonIds = new Set([...lessonIdMap.values()].filter((id) => existingLessonIds.has(id)));
|
||||||
|
const referencedExerciseIds = new Set(
|
||||||
|
(draft.exercises ?? []).map((exercise) => exercise.externalId).filter((id) => existingExerciseIds.has(id))
|
||||||
|
);
|
||||||
const courseUrl = await resolveCourseUrl(orgId, course.id, courseTitle);
|
const courseUrl = await resolveCourseUrl(orgId, course.id, courseTitle);
|
||||||
|
|
||||||
await updateCourseImportDraft(orgId, draftId, {
|
await updateCourseImportDraft(orgId, draftId, {
|
||||||
@@ -851,10 +970,13 @@ export async function publishCourseImportDraftToExistingCourseService(
|
|||||||
updatedSections,
|
updatedSections,
|
||||||
createdLessons,
|
createdLessons,
|
||||||
updatedLessons,
|
updatedLessons,
|
||||||
|
createdExercises,
|
||||||
|
updatedExercises,
|
||||||
createdLessonLanguages,
|
createdLessonLanguages,
|
||||||
updatedLessonLanguages,
|
updatedLessonLanguages,
|
||||||
untouchedSections: existingSections.length - referencedSectionIds.size,
|
untouchedSections: existingSections.length - referencedSectionIds.size,
|
||||||
untouchedLessons: existingLessons.length - referencedLessonIds.size,
|
untouchedLessons: existingLessons.length - referencedLessonIds.size,
|
||||||
|
untouchedExercises: existingExercises.length - referencedExerciseIds.size,
|
||||||
localeCount: new Set(draft.lessonLanguages.map((item) => item.locale)).size
|
localeCount: new Set(draft.lessonLanguages.map((item) => item.locale)).size
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export async function createExercise(data: TExerciseCreate): Promise<TExercise>
|
|||||||
title: q.question,
|
title: q.question,
|
||||||
questionTypeId: q.questionTypeId || 1,
|
questionTypeId: q.questionTypeId || 1,
|
||||||
points: q.points || 0,
|
points: q.points || 0,
|
||||||
|
order: typeof q.order === 'number' ? q.order : undefined,
|
||||||
settings: q.settings ?? {}
|
settings: q.settings ?? {}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ export async function createExercise(data: TExerciseCreate): Promise<TExercise>
|
|||||||
const question = questions[index];
|
const question = questions[index];
|
||||||
if (!question) return;
|
if (!question) return;
|
||||||
|
|
||||||
const optionsData: TNewOption[] = q.options.map((opt) => ({
|
const optionsData: TNewOption[] = (q.options ?? []).map((opt) => ({
|
||||||
questionId: question.id,
|
questionId: question.id,
|
||||||
label: opt.label,
|
label: opt.label,
|
||||||
isCorrect: opt.isCorrect,
|
isCorrect: opt.isCorrect,
|
||||||
@@ -233,6 +234,55 @@ export async function updateExerciseService(exerciseId: string, data: TExerciseU
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces an exercise's questions and options with the provided payload.
|
||||||
|
* Existing questions are deleted and recreated from the replacement payload.
|
||||||
|
*/
|
||||||
|
export async function replaceExerciseService(exerciseId: string, data: TExerciseUpdate): Promise<TExercise> {
|
||||||
|
try {
|
||||||
|
return await db.transaction(async (tx) => {
|
||||||
|
const txClient = tx as DbOrTxClient;
|
||||||
|
|
||||||
|
const exerciseUpdate = buildExerciseUpdateFields(data);
|
||||||
|
if (Object.keys(exerciseUpdate).length > 0) {
|
||||||
|
const updated = await updateExercise(exerciseId, exerciseUpdate, txClient);
|
||||||
|
if (!updated) {
|
||||||
|
throw new AppError('Failed to update exercise', ErrorCodes.INTERNAL_ERROR, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { questions: currentQuestions } = await fetchQuestionsAndOptions(exerciseId, txClient);
|
||||||
|
const currentQuestionIds = currentQuestions
|
||||||
|
.map((question) => question.id)
|
||||||
|
.filter((id): id is number => id !== undefined);
|
||||||
|
|
||||||
|
if (currentQuestionIds.length > 0) {
|
||||||
|
await deleteQuestionsByIds(currentQuestionIds, txClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
const replacementQuestions = data.questions ?? [];
|
||||||
|
const hasReplacementOptions = replacementQuestions.some((question) => (question.options?.length ?? 0) > 0);
|
||||||
|
|
||||||
|
if (replacementQuestions.length > 0) {
|
||||||
|
if (hasReplacementOptions) {
|
||||||
|
await syncOptionIdSequence(txClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
await createNewQuestionsWithOptions(exerciseId, replacementQuestions, txClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getExercise(exerciseId, txClient);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('replaceExerciseService error:', error);
|
||||||
|
if (error instanceof AppError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AppError('Failed to replace exercise', ErrorCodes.INTERNAL_ERROR, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes an exercise (cascade deletes questions and options)
|
* Deletes an exercise (cascade deletes questions and options)
|
||||||
* @param exerciseId Exercise ID
|
* @param exerciseId Exercise ID
|
||||||
|
|||||||
@@ -63,6 +63,17 @@ class AutomationApi extends BaseApiWithErrors {
|
|||||||
this.usage = { ...this.usage, activeKeys: this.usage.activeKeys + 1 };
|
this.usage = { ...this.usage, activeKeys: this.usage.activeKeys + 1 };
|
||||||
}
|
}
|
||||||
snackbar.success('snackbar.automation.created');
|
snackbar.success('snackbar.automation.created');
|
||||||
|
},
|
||||||
|
onError: (result) => {
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleValidationError(result);
|
||||||
|
|
||||||
|
if ('field' in result && result.field) {
|
||||||
|
this.errors[result.field] = result.error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,21 @@
|
|||||||
getClaudeCodeSnippet,
|
getClaudeCodeSnippet,
|
||||||
getCodexSnippet,
|
getCodexSnippet,
|
||||||
getCursorSnippet,
|
getCursorSnippet,
|
||||||
|
getOpenCodeSnippet,
|
||||||
getMaskedAutomationSecret
|
getMaskedAutomationSecret
|
||||||
} from '$features/automation/utils/automation-utils';
|
} from '$features/automation/utils/automation-utils';
|
||||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
import * as Alert from '@cio/ui/base/alert';
|
import * as Alert from '@cio/ui/base/alert';
|
||||||
import { Badge } from '@cio/ui/base/badge';
|
import { Badge } from '@cio/ui/base/badge';
|
||||||
|
import { Button } from '@cio/ui/base/button';
|
||||||
import * as Code from '@cio/ui/custom/code';
|
import * as Code from '@cio/ui/custom/code';
|
||||||
|
import * as Dialog from '@cio/ui/base/dialog';
|
||||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||||
import * as Field from '@cio/ui/base/field';
|
import * as Field from '@cio/ui/base/field';
|
||||||
import * as Table from '@cio/ui/base/table';
|
import * as Table from '@cio/ui/base/table';
|
||||||
import * as Tabs from '@cio/ui/base/tabs';
|
import * as Tabs from '@cio/ui/base/tabs';
|
||||||
|
import { InputField } from '@cio/ui/custom/input-field';
|
||||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||||
import CoinsIcon from '@lucide/svelte/icons/coins';
|
import CoinsIcon from '@lucide/svelte/icons/coins';
|
||||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||||
@@ -27,11 +31,29 @@
|
|||||||
|
|
||||||
let activeSetupTab = $state('cursor');
|
let activeSetupTab = $state('cursor');
|
||||||
let generatedSecret = $state<string | null>(null);
|
let generatedSecret = $state<string | null>(null);
|
||||||
|
let isCreateKeyModalOpen = $state(false);
|
||||||
|
let keyLabel = $state('');
|
||||||
|
|
||||||
|
const canCreateKey = $derived(keyLabel.trim().length > 0 && !automationApi.isLoading);
|
||||||
|
|
||||||
|
function resetCreateKeyModal() {
|
||||||
|
keyLabel = '';
|
||||||
|
automationApi.resetErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateKeyModal() {
|
||||||
|
resetCreateKeyModal();
|
||||||
|
isCreateKeyModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
async function onGenerateKey() {
|
async function onGenerateKey() {
|
||||||
await automationApi.createKey('mcp');
|
const result = await automationApi.createKey('mcp', keyLabel);
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
generatedSecret = automationApi.generatedSecret;
|
generatedSecret = automationApi.generatedSecret;
|
||||||
automationApi.clearGeneratedSecret();
|
automationApi.clearGeneratedSecret();
|
||||||
|
isCreateKeyModalOpen = false;
|
||||||
|
resetCreateKeyModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onRevokeKey(keyId: string) {
|
async function onRevokeKey(keyId: string) {
|
||||||
@@ -108,7 +130,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if $isOrgAdmin}
|
{#if $isOrgAdmin}
|
||||||
<IconButton
|
<IconButton
|
||||||
onclick={onGenerateKey}
|
onclick={openCreateKeyModal}
|
||||||
disabled={automationApi.isLoading}
|
disabled={automationApi.isLoading}
|
||||||
tooltip={$t('automation.mcp.keys.generate')}
|
tooltip={$t('automation.mcp.keys.generate')}
|
||||||
>
|
>
|
||||||
@@ -209,6 +231,7 @@
|
|||||||
<Tabs.Trigger value="cursor">{$t('automation.clients.cursor')}</Tabs.Trigger>
|
<Tabs.Trigger value="cursor">{$t('automation.clients.cursor')}</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="claude-code">{$t('automation.clients.claude_code')}</Tabs.Trigger>
|
<Tabs.Trigger value="claude-code">{$t('automation.clients.claude_code')}</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="codex">{$t('automation.clients.codex')}</Tabs.Trigger>
|
<Tabs.Trigger value="codex">{$t('automation.clients.codex')}</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="opencode">{$t('automation.clients.opencode')}</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|
||||||
<Tabs.Content value="claude-code" class="mt-4">
|
<Tabs.Content value="claude-code" class="mt-4">
|
||||||
@@ -234,6 +257,49 @@
|
|||||||
</Code.Root>
|
</Code.Root>
|
||||||
</Code.Overflow>
|
</Code.Overflow>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="opencode" class="mt-4">
|
||||||
|
<Code.Overflow>
|
||||||
|
<Code.Root code={getOpenCodeSnippet(generatedSecret)} lang="json">
|
||||||
|
<Code.CopyButton />
|
||||||
|
</Code.Root>
|
||||||
|
</Code.Overflow>
|
||||||
|
</Tabs.Content>
|
||||||
</Tabs.Root>
|
</Tabs.Root>
|
||||||
</Field.Set>
|
</Field.Set>
|
||||||
</Field.Group>
|
</Field.Group>
|
||||||
|
|
||||||
|
<Dialog.Root
|
||||||
|
bind:open={isCreateKeyModalOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
resetCreateKeyModal();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog.Content class="sm:max-w-md">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>{$t('automation.mcp.keys.modal.title')}</Dialog.Title>
|
||||||
|
<Dialog.Description>{$t('automation.mcp.keys.modal.description')}</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<InputField
|
||||||
|
label={$t('automation.mcp.keys.modal.name_label')}
|
||||||
|
placeholder={$t('automation.mcp.keys.modal.name_placeholder')}
|
||||||
|
bind:value={keyLabel}
|
||||||
|
errorMessage={automationApi.errors.label}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button variant="outline" onclick={() => (isCreateKeyModalOpen = false)}>
|
||||||
|
{$t('automation.mcp.keys.modal.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onclick={onGenerateKey} loading={automationApi.isLoading} disabled={!canCreateKey}>
|
||||||
|
{$t('automation.mcp.keys.modal.save')}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|||||||
@@ -30,6 +30,25 @@ export function getCodexSnippet(secret: string | null) {
|
|||||||
-- npx -y @classroomio/mcp`;
|
-- npx -y @classroomio/mcp`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getOpenCodeSnippet(secret: string | null) {
|
||||||
|
const apiKey = getAutomationSetupSecret(secret);
|
||||||
|
|
||||||
|
return `{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"classroomio": {
|
||||||
|
"type": "local",
|
||||||
|
"command": ["npx", "-y", "@classroomio/mcp"],
|
||||||
|
"enabled": true,
|
||||||
|
"environment": {
|
||||||
|
"CLASSROOMIO_API_URL": "https://api.classroomio.com",
|
||||||
|
"CLASSROOMIO_API_KEY": "${apiKey}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function getCursorSnippet(secret: string | null) {
|
export function getCursorSnippet(secret: string | null) {
|
||||||
const apiKey = getAutomationSetupSecret(secret);
|
const apiKey = getAutomationSetupSecret(secret);
|
||||||
|
|
||||||
|
|||||||
@@ -794,6 +794,17 @@
|
|||||||
"recipient_emails_placeholder": "[email protected], [email protected]",
|
"recipient_emails_placeholder": "[email protected], [email protected]",
|
||||||
"send_invite_emails": "Send invitationsmails med det samme",
|
"send_invite_emails": "Send invitationsmails med det samme",
|
||||||
"create_email_invites": "Opret e-mail-invitationer",
|
"create_email_invites": "Opret e-mail-invitationer",
|
||||||
|
"existing_students_title": "Add existing org students",
|
||||||
|
"existing_students_description": "Choose student members already in this organization and grant them direct access to this course.",
|
||||||
|
"existing_students_search": "Search organization students",
|
||||||
|
"loading_existing_students": "Loading organization students...",
|
||||||
|
"no_existing_students": "No organization students are available to add.",
|
||||||
|
"no_matching_existing_students": "No organization students match your search.",
|
||||||
|
"notify_existing_students": "Send course access email",
|
||||||
|
"grant_access": "Grant Access",
|
||||||
|
"invite_new_students_title": "Invite new students",
|
||||||
|
"invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.",
|
||||||
|
"invite_new_students": "Invite Students",
|
||||||
"existing_invites": "Eksisterende invitationer",
|
"existing_invites": "Eksisterende invitationer",
|
||||||
"refresh": "Opdater",
|
"refresh": "Opdater",
|
||||||
"loading_invites": "Indlæser invitationer...",
|
"loading_invites": "Indlæser invitationer...",
|
||||||
@@ -827,6 +838,7 @@
|
|||||||
"snackbar_failed_load_audit": "Invitationsrevision kunne ikke indlæses",
|
"snackbar_failed_load_audit": "Invitationsrevision kunne ikke indlæses",
|
||||||
"snackbar_metadata_copied": "Invitationsmetadata er kopieret",
|
"snackbar_metadata_copied": "Invitationsmetadata er kopieret",
|
||||||
"snackbar_email_invites_success": "Oprettet {count} invitation(er). Sendt {sent}, mislykkedes {failed}. Sprang over {duplikater} dubletter.",
|
"snackbar_email_invites_success": "Oprettet {count} invitation(er). Sendt {sent}, mislykkedes {failed}. Sprang over {duplikater} dubletter.",
|
||||||
|
"select_at_least_one_student": "Select at least one existing student",
|
||||||
"presets": {
|
"presets": {
|
||||||
"one_time_24h": "Engangs (24 timer)",
|
"one_time_24h": "Engangs (24 timer)",
|
||||||
"multi_use_7d": "Multi-brug (7 dage)",
|
"multi_use_7d": "Multi-brug (7 dage)",
|
||||||
@@ -2492,13 +2504,22 @@
|
|||||||
"description": "Brug én nøgle med organisationsomfang pr. integration. Hvis en nøgle roteres eller tilbagekaldes, holder klienten op med at arbejde, indtil den nye hemmelighed er konfigureret.",
|
"description": "Brug én nøgle med organisationsomfang pr. integration. Hvis en nøgle roteres eller tilbagekaldes, holder klienten op med at arbejde, indtil den nye hemmelighed er konfigureret.",
|
||||||
"claude_code_description": "Registrerer `@classroomio/mcp` med Claude Code ved hjælp af den genererede org-omfangede nøgle.",
|
"claude_code_description": "Registrerer `@classroomio/mcp` med Claude Code ved hjælp af den genererede org-omfangede nøgle.",
|
||||||
"codex_description": "Tilføjer ClassroomIO MCP-serveren til Codex med den samme org-omfattede nøgle.",
|
"codex_description": "Tilføjer ClassroomIO MCP-serveren til Codex med den samme org-omfattede nøgle.",
|
||||||
"cursor_description": "Indsæt dette i `~/.cursor/mcp.json` eller dit arbejdsområde MCP-konfiguration."
|
"cursor_description": "Indsæt dette i `~/.cursor/mcp.json` eller dit arbejdsområde MCP-konfiguration.",
|
||||||
|
"opencode_description": "Tilføj dette til `opencode.json` i projektroden eller `~/.config/opencode/opencode.json`."
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
"title": "MCP nøgler",
|
"title": "MCP nøgler",
|
||||||
"description": "Disse nøgler autentificerer den aktuelle organisation, når en MCP-klient ringer til ClassroomIO gennem `@classroomio/mcp`.",
|
"description": "Disse nøgler autentificerer den aktuelle organisation, når en MCP-klient ringer til ClassroomIO gennem `@classroomio/mcp`.",
|
||||||
"generate": "Generer MCP-nøgle",
|
"generate": "Generer MCP-nøgle",
|
||||||
"secret_once": "Denne hemmelighed vises én gang. Kopier det til din MCP-klient nu."
|
"secret_once": "Denne hemmelighed vises én gang. Kopier det til din MCP-klient nu.",
|
||||||
|
"modal": {
|
||||||
|
"title": "Create MCP key",
|
||||||
|
"description": "Choose a name for this MCP key before it is generated.",
|
||||||
|
"name_label": "Key name",
|
||||||
|
"name_placeholder": "ClassroomIO OpenCode",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Create key"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
@@ -2507,7 +2528,7 @@
|
|||||||
"rotate": "Roter",
|
"rotate": "Roter",
|
||||||
"revoke": "Tilbagekald",
|
"revoke": "Tilbagekald",
|
||||||
"empty_title": "Ingen MCP-nøgler endnu",
|
"empty_title": "Ingen MCP-nøgler endnu",
|
||||||
"empty_description": "Opret den første org-omfattede MCP-nøgle til at forbinde Claude Code, Codex eller Cursor.",
|
"empty_description": "Opret den første org-omfattede MCP-nøgle til at forbinde Claude Code, Codex, Cursor eller OpenCode.",
|
||||||
"created_at_label": "Oprettet:",
|
"created_at_label": "Oprettet:",
|
||||||
"last_used_label": "Sidst brugt:",
|
"last_used_label": "Sidst brugt:",
|
||||||
"never_used": "Aldrig",
|
"never_used": "Aldrig",
|
||||||
@@ -2518,7 +2539,8 @@
|
|||||||
"clients": {
|
"clients": {
|
||||||
"claude_code": "Claude kode",
|
"claude_code": "Claude kode",
|
||||||
"codex": "Codex",
|
"codex": "Codex",
|
||||||
"cursor": "Markør"
|
"cursor": "Markør",
|
||||||
|
"opencode": "OpenCode"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"mcp": "MCP",
|
"mcp": "MCP",
|
||||||
|
|||||||
@@ -794,6 +794,17 @@
|
|||||||
"recipient_emails_placeholder": "[email protected], [email protected]",
|
"recipient_emails_placeholder": "[email protected], [email protected]",
|
||||||
"send_invite_emails": "Senden Sie sofort Einladungs-E-Mails",
|
"send_invite_emails": "Senden Sie sofort Einladungs-E-Mails",
|
||||||
"create_email_invites": "Erstellen Sie E-Mail-Einladungen",
|
"create_email_invites": "Erstellen Sie E-Mail-Einladungen",
|
||||||
|
"existing_students_title": "Add existing org students",
|
||||||
|
"existing_students_description": "Choose student members already in this organization and grant them direct access to this course.",
|
||||||
|
"existing_students_search": "Search organization students",
|
||||||
|
"loading_existing_students": "Loading organization students...",
|
||||||
|
"no_existing_students": "No organization students are available to add.",
|
||||||
|
"no_matching_existing_students": "No organization students match your search.",
|
||||||
|
"notify_existing_students": "Send course access email",
|
||||||
|
"grant_access": "Grant Access",
|
||||||
|
"invite_new_students_title": "Invite new students",
|
||||||
|
"invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.",
|
||||||
|
"invite_new_students": "Invite Students",
|
||||||
"existing_invites": "Vorhandene Einladungen",
|
"existing_invites": "Vorhandene Einladungen",
|
||||||
"refresh": "Aktualisieren",
|
"refresh": "Aktualisieren",
|
||||||
"loading_invites": "Einladungen werden geladen...",
|
"loading_invites": "Einladungen werden geladen...",
|
||||||
@@ -827,6 +838,7 @@
|
|||||||
"snackbar_failed_load_audit": "Das Einladungsaudit konnte nicht geladen werden",
|
"snackbar_failed_load_audit": "Das Einladungsaudit konnte nicht geladen werden",
|
||||||
"snackbar_metadata_copied": "Einladungsmetadaten kopiert",
|
"snackbar_metadata_copied": "Einladungsmetadaten kopiert",
|
||||||
"snackbar_email_invites_success": "{count} Einladung(en) erstellt. Gesendet {sent}, fehlgeschlagen {failed}. {duplicates} Duplikat(e) übersprungen.",
|
"snackbar_email_invites_success": "{count} Einladung(en) erstellt. Gesendet {sent}, fehlgeschlagen {failed}. {duplicates} Duplikat(e) übersprungen.",
|
||||||
|
"select_at_least_one_student": "Select at least one existing student",
|
||||||
"presets": {
|
"presets": {
|
||||||
"one_time_24h": "Einmalig (24h)",
|
"one_time_24h": "Einmalig (24h)",
|
||||||
"multi_use_7d": "Mehrfachnutzung (7 Tage)",
|
"multi_use_7d": "Mehrfachnutzung (7 Tage)",
|
||||||
@@ -2492,13 +2504,22 @@
|
|||||||
"description": "Verwenden Sie einen organisationsweiten Schlüssel pro Integration. Wenn ein Schlüssel rotiert oder widerrufen wird, funktioniert der Client nicht mehr, bis das neue Geheimnis konfiguriert ist.",
|
"description": "Verwenden Sie einen organisationsweiten Schlüssel pro Integration. Wenn ein Schlüssel rotiert oder widerrufen wird, funktioniert der Client nicht mehr, bis das neue Geheimnis konfiguriert ist.",
|
||||||
"claude_code_description": "Registriert „@classroomio/mcp“ bei Claude Code unter Verwendung des generierten organisationsweit gültigen Schlüssels.",
|
"claude_code_description": "Registriert „@classroomio/mcp“ bei Claude Code unter Verwendung des generierten organisationsweit gültigen Schlüssels.",
|
||||||
"codex_description": "Fügt den ClassroomIO MCP-Server mit demselben organisationsweit gültigen Schlüssel zu Codex hinzu.",
|
"codex_description": "Fügt den ClassroomIO MCP-Server mit demselben organisationsweit gültigen Schlüssel zu Codex hinzu.",
|
||||||
"cursor_description": "Fügen Sie dies in „~/.cursor/mcp.json“ oder Ihre Workspace-MCP-Konfiguration ein."
|
"cursor_description": "Fügen Sie dies in „~/.cursor/mcp.json“ oder Ihre Workspace-MCP-Konfiguration ein.",
|
||||||
|
"opencode_description": "Fügen Sie dies in `opencode.json` im Projektstamm oder in `~/.config/opencode/opencode.json` ein."
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
"title": "MCP-Schlüssel",
|
"title": "MCP-Schlüssel",
|
||||||
"description": "Diese Schlüssel authentifizieren die aktuelle Organisation, wenn ein MCP-Client ClassroomIO über „@classroomio/mcp“ aufruft.",
|
"description": "Diese Schlüssel authentifizieren die aktuelle Organisation, wenn ein MCP-Client ClassroomIO über „@classroomio/mcp“ aufruft.",
|
||||||
"generate": "MCP-Schlüssel generieren",
|
"generate": "MCP-Schlüssel generieren",
|
||||||
"secret_once": "Dieses Geheimnis wird einmal gezeigt. Kopieren Sie es jetzt in Ihren MCP-Client."
|
"secret_once": "Dieses Geheimnis wird einmal gezeigt. Kopieren Sie es jetzt in Ihren MCP-Client.",
|
||||||
|
"modal": {
|
||||||
|
"title": "Create MCP key",
|
||||||
|
"description": "Choose a name for this MCP key before it is generated.",
|
||||||
|
"name_label": "Key name",
|
||||||
|
"name_placeholder": "ClassroomIO OpenCode",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Create key"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
@@ -2507,7 +2528,7 @@
|
|||||||
"rotate": "Drehen",
|
"rotate": "Drehen",
|
||||||
"revoke": "Widerrufen",
|
"revoke": "Widerrufen",
|
||||||
"empty_title": "Noch keine MCP-Schlüssel",
|
"empty_title": "Noch keine MCP-Schlüssel",
|
||||||
"empty_description": "Erstellen Sie den ersten organisationsweit gültigen MCP-Schlüssel, um Claude Code, Codex oder Cursor zu verbinden.",
|
"empty_description": "Erstellen Sie den ersten organisationsweit gültigen MCP-Schlüssel, um Claude Code, Codex, Cursor oder OpenCode zu verbinden.",
|
||||||
"created_at_label": "Erstellt:",
|
"created_at_label": "Erstellt:",
|
||||||
"last_used_label": "Zuletzt verwendet:",
|
"last_used_label": "Zuletzt verwendet:",
|
||||||
"never_used": "Niemals",
|
"never_used": "Niemals",
|
||||||
@@ -2518,7 +2539,8 @@
|
|||||||
"clients": {
|
"clients": {
|
||||||
"claude_code": "Claude Code",
|
"claude_code": "Claude Code",
|
||||||
"codex": "Kodex",
|
"codex": "Kodex",
|
||||||
"cursor": "Cursor"
|
"cursor": "Cursor",
|
||||||
|
"opencode": "OpenCode"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"mcp": "MCP",
|
"mcp": "MCP",
|
||||||
|
|||||||
@@ -1074,6 +1074,9 @@
|
|||||||
},
|
},
|
||||||
"download_pdf": "Download PDF",
|
"download_pdf": "Download PDF",
|
||||||
"autosaving": "Autosaving...",
|
"autosaving": "Autosaving...",
|
||||||
|
"saving": "Saving…",
|
||||||
|
"saved": "Saved",
|
||||||
|
"draft_recovery": "An unsaved draft was found for this lesson. Would you like to restore it?",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"mark_as": "Mark as",
|
"mark_as": "Mark as",
|
||||||
@@ -1652,7 +1655,7 @@
|
|||||||
"select": "Select tutors in organization...",
|
"select": "Select tutors in organization...",
|
||||||
"to_add": "To add tutor to your organization,",
|
"to_add": "To add tutor to your organization,",
|
||||||
"go_to": "go to organization settings",
|
"go_to": "go to organization settings",
|
||||||
"invite_students": "Invite Students",
|
"invite_students": "Add Students",
|
||||||
"via_link": "You can invite students via an invite link",
|
"via_link": "You can invite students via an invite link",
|
||||||
"via_qr": "You can invite students via QR",
|
"via_qr": "You can invite students via QR",
|
||||||
"download_qr": "Download QR code",
|
"download_qr": "Download QR code",
|
||||||
@@ -1683,6 +1686,17 @@
|
|||||||
"recipient_emails_placeholder": "[email protected], [email protected]",
|
"recipient_emails_placeholder": "[email protected], [email protected]",
|
||||||
"send_invite_emails": "Send invite emails immediately",
|
"send_invite_emails": "Send invite emails immediately",
|
||||||
"create_email_invites": "Create Email Invites",
|
"create_email_invites": "Create Email Invites",
|
||||||
|
"existing_students_title": "Add existing org students",
|
||||||
|
"existing_students_description": "Choose student members already in this organization and grant them direct access to this course.",
|
||||||
|
"existing_students_search": "Search organization students",
|
||||||
|
"loading_existing_students": "Loading organization students...",
|
||||||
|
"no_existing_students": "No organization students are available to add.",
|
||||||
|
"no_matching_existing_students": "No organization students match your search.",
|
||||||
|
"notify_existing_students": "Send course access email",
|
||||||
|
"grant_access": "Grant Access",
|
||||||
|
"invite_new_students_title": "Invite new students",
|
||||||
|
"invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.",
|
||||||
|
"invite_new_students": "Invite Students",
|
||||||
"existing_invites": "Existing Invites",
|
"existing_invites": "Existing Invites",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"loading_invites": "Loading invites...",
|
"loading_invites": "Loading invites...",
|
||||||
@@ -1715,7 +1729,8 @@
|
|||||||
"snackbar_failed_revoke_invite": "Failed to revoke invite",
|
"snackbar_failed_revoke_invite": "Failed to revoke invite",
|
||||||
"snackbar_failed_load_audit": "Failed to load invite audit",
|
"snackbar_failed_load_audit": "Failed to load invite audit",
|
||||||
"snackbar_metadata_copied": "Invite metadata copied",
|
"snackbar_metadata_copied": "Invite metadata copied",
|
||||||
"snackbar_email_invites_success": "Created {count} invite(s). Sent {sent}, failed {failed}. Skipped {duplicates} duplicate(s)."
|
"snackbar_email_invites_success": "Created {count} invite(s). Sent {sent}, failed {failed}. Skipped {duplicates} duplicate(s).",
|
||||||
|
"select_at_least_one_student": "Select at least one existing student"
|
||||||
},
|
},
|
||||||
"delete_confirmation": {
|
"delete_confirmation": {
|
||||||
"title": "Delete",
|
"title": "Delete",
|
||||||
@@ -1995,12 +2010,45 @@
|
|||||||
"audience": {
|
"audience": {
|
||||||
"title": "Audience",
|
"title": "Audience",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
|
"import_users": "Import Users",
|
||||||
"no_audience": "No audience to manage",
|
"no_audience": "No audience to manage",
|
||||||
"manage": "Manage all your students here and stay connected with them",
|
"manage": "Manage all your students here and stay connected with them",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"date_joined": "Date Joined",
|
"date_joined": "Date Joined",
|
||||||
"upgrade": "Upgrade your plan to invite more students"
|
"upgrade": "Upgrade your plan to invite more students",
|
||||||
|
"select_all": "Select all",
|
||||||
|
"selected_count": "{count} selected",
|
||||||
|
"assign_courses": "Assign to Courses",
|
||||||
|
"import": {
|
||||||
|
"title": "Import Users",
|
||||||
|
"description": "Add students to your organization by entering their email addresses. Optionally assign them to courses and send invitation emails.",
|
||||||
|
"emails_label": "Email Addresses",
|
||||||
|
"emails_placeholder": "Enter emails separated by commas, newlines, or semicolons\ne.g. [email protected], [email protected]",
|
||||||
|
"course_access": "Course Access",
|
||||||
|
"no_courses": "No course assignment",
|
||||||
|
"all_courses": "All courses",
|
||||||
|
"select_courses": "Select specific courses",
|
||||||
|
"select_courses_placeholder": "Choose courses...",
|
||||||
|
"send_email": "Send invitation email to imported users",
|
||||||
|
"submit": "Import Users",
|
||||||
|
"back": "Back to Audience",
|
||||||
|
"snackbar_success": "Imported {imported} users ({skipped} already existed, {emailsSent} emails sent)",
|
||||||
|
"snackbar_no_emails": "Please enter at least one email address",
|
||||||
|
"snackbar_failed": "Failed to import users"
|
||||||
|
},
|
||||||
|
"assign": {
|
||||||
|
"title": "Assign to Courses",
|
||||||
|
"description": "Give the selected users access to courses and optionally notify them by email.",
|
||||||
|
"select_courses": "Select courses",
|
||||||
|
"select_courses_placeholder": "Choose courses...",
|
||||||
|
"send_email": "Send notification email",
|
||||||
|
"submit": "Assign",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"snackbar_success": "Assigned {assigned} users to courses ({alreadyEnrolled} already enrolled, {emailsSent} emails sent)",
|
||||||
|
"snackbar_no_courses": "Please select at least one course",
|
||||||
|
"snackbar_failed": "Failed to assign courses"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
"title": "Analytics",
|
"title": "Analytics",
|
||||||
@@ -2088,7 +2136,8 @@
|
|||||||
"clients": {
|
"clients": {
|
||||||
"claude_code": "Claude Code",
|
"claude_code": "Claude Code",
|
||||||
"codex": "Codex",
|
"codex": "Codex",
|
||||||
"cursor": "Cursor"
|
"cursor": "Cursor",
|
||||||
|
"opencode": "OpenCode"
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
@@ -2096,7 +2145,7 @@
|
|||||||
"rotate": "Rotate",
|
"rotate": "Rotate",
|
||||||
"revoke": "Revoke",
|
"revoke": "Revoke",
|
||||||
"empty_title": "No MCP keys yet",
|
"empty_title": "No MCP keys yet",
|
||||||
"empty_description": "Create the first org-scoped MCP key to connect Claude Code, Codex, or Cursor.",
|
"empty_description": "Create the first org-scoped MCP key to connect Claude Code, Codex, Cursor, or OpenCode.",
|
||||||
"created_at_label": "Created:",
|
"created_at_label": "Created:",
|
||||||
"last_used_label": "Last used:",
|
"last_used_label": "Last used:",
|
||||||
"never_used": "Never",
|
"never_used": "Never",
|
||||||
@@ -2124,14 +2173,23 @@
|
|||||||
"title": "MCP keys",
|
"title": "MCP keys",
|
||||||
"description": "These keys authenticate the current organization when an MCP client calls ClassroomIO through `@classroomio/mcp`.",
|
"description": "These keys authenticate the current organization when an MCP client calls ClassroomIO through `@classroomio/mcp`.",
|
||||||
"generate": "Generate MCP key",
|
"generate": "Generate MCP key",
|
||||||
"secret_once": "This secret is shown once. Copy it into your MCP client now."
|
"secret_once": "This secret is shown once. Copy it into your MCP client now.",
|
||||||
|
"modal": {
|
||||||
|
"title": "Create MCP key",
|
||||||
|
"description": "Choose a name for this MCP key before it is generated.",
|
||||||
|
"name_label": "Key name",
|
||||||
|
"name_placeholder": "ClassroomIO OpenCode",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Create key"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
"title": "Client setup",
|
"title": "Client setup",
|
||||||
"description": "Use one org-scoped key per integration. If a key is rotated or revoked, the client stops working until the new secret is configured.",
|
"description": "Use one org-scoped key per integration. If a key is rotated or revoked, the client stops working until the new secret is configured.",
|
||||||
"claude_code_description": "Registers `@classroomio/mcp` with Claude Code using the generated org-scoped key.",
|
"claude_code_description": "Registers `@classroomio/mcp` with Claude Code using the generated org-scoped key.",
|
||||||
"codex_description": "Adds the ClassroomIO MCP server to Codex with the same org-scoped key.",
|
"codex_description": "Adds the ClassroomIO MCP server to Codex with the same org-scoped key.",
|
||||||
"cursor_description": "Paste this into `~/.cursor/mcp.json` or your workspace MCP config."
|
"cursor_description": "Paste this into `~/.cursor/mcp.json` or your workspace MCP config.",
|
||||||
|
"opencode_description": "Add this to `opencode.json` in your project root or `~/.config/opencode/opencode.json`."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -794,6 +794,17 @@
|
|||||||
"recipient_emails_placeholder": "[email protected], [email protected]",
|
"recipient_emails_placeholder": "[email protected], [email protected]",
|
||||||
"send_invite_emails": "Enviar correos electrónicos de invitación inmediatamente",
|
"send_invite_emails": "Enviar correos electrónicos de invitación inmediatamente",
|
||||||
"create_email_invites": "Crear invitaciones por correo electrónico",
|
"create_email_invites": "Crear invitaciones por correo electrónico",
|
||||||
|
"existing_students_title": "Add existing org students",
|
||||||
|
"existing_students_description": "Choose student members already in this organization and grant them direct access to this course.",
|
||||||
|
"existing_students_search": "Search organization students",
|
||||||
|
"loading_existing_students": "Loading organization students...",
|
||||||
|
"no_existing_students": "No organization students are available to add.",
|
||||||
|
"no_matching_existing_students": "No organization students match your search.",
|
||||||
|
"notify_existing_students": "Send course access email",
|
||||||
|
"grant_access": "Grant Access",
|
||||||
|
"invite_new_students_title": "Invite new students",
|
||||||
|
"invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.",
|
||||||
|
"invite_new_students": "Invite Students",
|
||||||
"existing_invites": "Invitaciones existentes",
|
"existing_invites": "Invitaciones existentes",
|
||||||
"refresh": "Actualizar",
|
"refresh": "Actualizar",
|
||||||
"loading_invites": "Cargando invitaciones...",
|
"loading_invites": "Cargando invitaciones...",
|
||||||
@@ -827,6 +838,7 @@
|
|||||||
"snackbar_failed_load_audit": "No se pudo cargar la auditoría de invitación",
|
"snackbar_failed_load_audit": "No se pudo cargar la auditoría de invitación",
|
||||||
"snackbar_metadata_copied": "Metadatos de invitación copiados",
|
"snackbar_metadata_copied": "Metadatos de invitación copiados",
|
||||||
"snackbar_email_invites_success": "Se crearon {count} invitaciones. Enviado {enviado}, falló {falló}. Se omitieron {duplicados} duplicados.",
|
"snackbar_email_invites_success": "Se crearon {count} invitaciones. Enviado {enviado}, falló {falló}. Se omitieron {duplicados} duplicados.",
|
||||||
|
"select_at_least_one_student": "Select at least one existing student",
|
||||||
"presets": {
|
"presets": {
|
||||||
"one_time_24h": "Una vez (24h)",
|
"one_time_24h": "Una vez (24h)",
|
||||||
"multi_use_7d": "Multiuso (7 días)",
|
"multi_use_7d": "Multiuso (7 días)",
|
||||||
@@ -2492,13 +2504,22 @@
|
|||||||
"description": "Utilice una clave con ámbito de organización por integración. Si se rota o revoca una clave, el cliente deja de funcionar hasta que se configura el nuevo secreto.",
|
"description": "Utilice una clave con ámbito de organización por integración. Si se rota o revoca una clave, el cliente deja de funcionar hasta que se configura el nuevo secreto.",
|
||||||
"claude_code_description": "Registra `@classroomio/mcp` con Claude Code utilizando la clave de alcance organizacional generada.",
|
"claude_code_description": "Registra `@classroomio/mcp` con Claude Code utilizando la clave de alcance organizacional generada.",
|
||||||
"codex_description": "Agrega el servidor ClassroomIO MCP al Codex con la misma clave de ámbito de organización.",
|
"codex_description": "Agrega el servidor ClassroomIO MCP al Codex con la misma clave de ámbito de organización.",
|
||||||
"cursor_description": "Pegue esto en `~/.cursor/mcp.json` o en la configuración MCP de su espacio de trabajo."
|
"cursor_description": "Pegue esto en `~/.cursor/mcp.json` o en la configuración MCP de su espacio de trabajo.",
|
||||||
|
"opencode_description": "Agregue esto a `opencode.json` en la raíz del proyecto o a `~/.config/opencode/opencode.json`."
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
"title": "Claves MCP",
|
"title": "Claves MCP",
|
||||||
"description": "Estas claves autentican la organización actual cuando un cliente MCP llama a ClassroomIO a través de `@classroomio/mcp`.",
|
"description": "Estas claves autentican la organización actual cuando un cliente MCP llama a ClassroomIO a través de `@classroomio/mcp`.",
|
||||||
"generate": "Generar clave MCP",
|
"generate": "Generar clave MCP",
|
||||||
"secret_once": "Este secreto se muestra una vez. Cópielo en su cliente MCP ahora."
|
"secret_once": "Este secreto se muestra una vez. Cópielo en su cliente MCP ahora.",
|
||||||
|
"modal": {
|
||||||
|
"title": "Create MCP key",
|
||||||
|
"description": "Choose a name for this MCP key before it is generated.",
|
||||||
|
"name_label": "Key name",
|
||||||
|
"name_placeholder": "ClassroomIO OpenCode",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Create key"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
@@ -2507,7 +2528,7 @@
|
|||||||
"rotate": "rotar",
|
"rotate": "rotar",
|
||||||
"revoke": "Revocar",
|
"revoke": "Revocar",
|
||||||
"empty_title": "Aún no hay claves MCP",
|
"empty_title": "Aún no hay claves MCP",
|
||||||
"empty_description": "Cree la primera clave MCP con ámbito de organización para conectar Claude Code, Codex o Cursor.",
|
"empty_description": "Cree la primera clave MCP con ámbito de organización para conectar Claude Code, Codex, Cursor u OpenCode.",
|
||||||
"created_at_label": "Creado:",
|
"created_at_label": "Creado:",
|
||||||
"last_used_label": "Último usado:",
|
"last_used_label": "Último usado:",
|
||||||
"never_used": "nunca",
|
"never_used": "nunca",
|
||||||
@@ -2518,7 +2539,8 @@
|
|||||||
"clients": {
|
"clients": {
|
||||||
"claude_code": "Código Claude",
|
"claude_code": "Código Claude",
|
||||||
"codex": "Códice",
|
"codex": "Códice",
|
||||||
"cursor": "Cursor"
|
"cursor": "Cursor",
|
||||||
|
"opencode": "OpenCode"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"mcp": "MCP",
|
"mcp": "MCP",
|
||||||
|
|||||||
@@ -794,6 +794,17 @@
|
|||||||
"recipient_emails_placeholder": "é[email protected], é[email protected]",
|
"recipient_emails_placeholder": "é[email protected], é[email protected]",
|
||||||
"send_invite_emails": "Envoyer immédiatement des e-mails d'invitation",
|
"send_invite_emails": "Envoyer immédiatement des e-mails d'invitation",
|
||||||
"create_email_invites": "Créer des invitations par e-mail",
|
"create_email_invites": "Créer des invitations par e-mail",
|
||||||
|
"existing_students_title": "Add existing org students",
|
||||||
|
"existing_students_description": "Choose student members already in this organization and grant them direct access to this course.",
|
||||||
|
"existing_students_search": "Search organization students",
|
||||||
|
"loading_existing_students": "Loading organization students...",
|
||||||
|
"no_existing_students": "No organization students are available to add.",
|
||||||
|
"no_matching_existing_students": "No organization students match your search.",
|
||||||
|
"notify_existing_students": "Send course access email",
|
||||||
|
"grant_access": "Grant Access",
|
||||||
|
"invite_new_students_title": "Invite new students",
|
||||||
|
"invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.",
|
||||||
|
"invite_new_students": "Invite Students",
|
||||||
"existing_invites": "Invitations existantes",
|
"existing_invites": "Invitations existantes",
|
||||||
"refresh": "Actualiser",
|
"refresh": "Actualiser",
|
||||||
"loading_invites": "Chargement des invitations...",
|
"loading_invites": "Chargement des invitations...",
|
||||||
@@ -827,6 +838,7 @@
|
|||||||
"snackbar_failed_load_audit": "Échec du chargement de l'audit d'invitation",
|
"snackbar_failed_load_audit": "Échec du chargement de l'audit d'invitation",
|
||||||
"snackbar_metadata_copied": "Métadonnées d'invitation copiées",
|
"snackbar_metadata_copied": "Métadonnées d'invitation copiées",
|
||||||
"snackbar_email_invites_success": "{count} invitation(s) créée(s). Envoyé {envoyé}, échec {échec}. {duplicates} doublon(s) ignoré(s).",
|
"snackbar_email_invites_success": "{count} invitation(s) créée(s). Envoyé {envoyé}, échec {échec}. {duplicates} doublon(s) ignoré(s).",
|
||||||
|
"select_at_least_one_student": "Select at least one existing student",
|
||||||
"presets": {
|
"presets": {
|
||||||
"one_time_24h": "Une fois (24h)",
|
"one_time_24h": "Une fois (24h)",
|
||||||
"multi_use_7d": "Multi-usage (7 jours)",
|
"multi_use_7d": "Multi-usage (7 jours)",
|
||||||
@@ -2492,13 +2504,22 @@
|
|||||||
"description": "Utilisez une clé à l’échelle de l’organisation par intégration. Si une clé est alternée ou révoquée, le client cesse de fonctionner jusqu'à ce que le nouveau secret soit configuré.",
|
"description": "Utilisez une clé à l’échelle de l’organisation par intégration. Si une clé est alternée ou révoquée, le client cesse de fonctionner jusqu'à ce que le nouveau secret soit configuré.",
|
||||||
"claude_code_description": "Enregistre `@classroomio/mcp` auprès de Claude Code à l'aide de la clé générée à l'échelle de l'organisation.",
|
"claude_code_description": "Enregistre `@classroomio/mcp` auprès de Claude Code à l'aide de la clé générée à l'échelle de l'organisation.",
|
||||||
"codex_description": "Ajoute le serveur ClassroomIO MCP au Codex avec la même clé de portée organisationnelle.",
|
"codex_description": "Ajoute le serveur ClassroomIO MCP au Codex avec la même clé de portée organisationnelle.",
|
||||||
"cursor_description": "Collez-le dans `~/.cursor/mcp.json` ou dans la configuration MCP de votre espace de travail."
|
"cursor_description": "Collez-le dans `~/.cursor/mcp.json` ou dans la configuration MCP de votre espace de travail.",
|
||||||
|
"opencode_description": "Ajoutez ceci à `opencode.json` à la racine du projet ou à `~/.config/opencode/opencode.json`."
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
"title": "Clés MCP",
|
"title": "Clés MCP",
|
||||||
"description": "Ces clés authentifient l'organisation actuelle lorsqu'un client MCP appelle ClassroomIO via `@classroomio/mcp`.",
|
"description": "Ces clés authentifient l'organisation actuelle lorsqu'un client MCP appelle ClassroomIO via `@classroomio/mcp`.",
|
||||||
"generate": "Générer une clé MCP",
|
"generate": "Générer une clé MCP",
|
||||||
"secret_once": "Ce secret est montré une fois. Copiez-le dans votre client MCP maintenant."
|
"secret_once": "Ce secret est montré une fois. Copiez-le dans votre client MCP maintenant.",
|
||||||
|
"modal": {
|
||||||
|
"title": "Create MCP key",
|
||||||
|
"description": "Choose a name for this MCP key before it is generated.",
|
||||||
|
"name_label": "Key name",
|
||||||
|
"name_placeholder": "ClassroomIO OpenCode",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Create key"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
@@ -2507,7 +2528,7 @@
|
|||||||
"rotate": "Rotation",
|
"rotate": "Rotation",
|
||||||
"revoke": "Révoquer",
|
"revoke": "Révoquer",
|
||||||
"empty_title": "Pas encore de clés MCP",
|
"empty_title": "Pas encore de clés MCP",
|
||||||
"empty_description": "Créez la première clé MCP à l'échelle de l'organisation pour connecter Claude Code, Codex ou Cursor.",
|
"empty_description": "Créez la première clé MCP à l'échelle de l'organisation pour connecter Claude Code, Codex, Cursor ou OpenCode.",
|
||||||
"created_at_label": "Créé :",
|
"created_at_label": "Créé :",
|
||||||
"last_used_label": "Dernière utilisation :",
|
"last_used_label": "Dernière utilisation :",
|
||||||
"never_used": "Jamais",
|
"never_used": "Jamais",
|
||||||
@@ -2518,7 +2539,8 @@
|
|||||||
"clients": {
|
"clients": {
|
||||||
"claude_code": "Claude Code",
|
"claude_code": "Claude Code",
|
||||||
"codex": "Codex",
|
"codex": "Codex",
|
||||||
"cursor": "Curseur"
|
"cursor": "Curseur",
|
||||||
|
"opencode": "OpenCode"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"mcp": "PCM",
|
"mcp": "PCM",
|
||||||
|
|||||||
@@ -794,6 +794,17 @@
|
|||||||
"recipient_emails_placeholder": "[email protected], [email protected]",
|
"recipient_emails_placeholder": "[email protected], [email protected]",
|
||||||
"send_invite_emails": "तुरंत आमंत्रण ईमेल भेजें",
|
"send_invite_emails": "तुरंत आमंत्रण ईमेल भेजें",
|
||||||
"create_email_invites": "ईमेल आमंत्रण बनाएँ",
|
"create_email_invites": "ईमेल आमंत्रण बनाएँ",
|
||||||
|
"existing_students_title": "Add existing org students",
|
||||||
|
"existing_students_description": "Choose student members already in this organization and grant them direct access to this course.",
|
||||||
|
"existing_students_search": "Search organization students",
|
||||||
|
"loading_existing_students": "Loading organization students...",
|
||||||
|
"no_existing_students": "No organization students are available to add.",
|
||||||
|
"no_matching_existing_students": "No organization students match your search.",
|
||||||
|
"notify_existing_students": "Send course access email",
|
||||||
|
"grant_access": "Grant Access",
|
||||||
|
"invite_new_students_title": "Invite new students",
|
||||||
|
"invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.",
|
||||||
|
"invite_new_students": "Invite Students",
|
||||||
"existing_invites": "मौजूदा आमंत्रण",
|
"existing_invites": "मौजूदा आमंत्रण",
|
||||||
"refresh": "ताज़ा करें",
|
"refresh": "ताज़ा करें",
|
||||||
"loading_invites": "आमंत्रण लोड हो रहे हैं...",
|
"loading_invites": "आमंत्रण लोड हो रहे हैं...",
|
||||||
@@ -827,6 +838,7 @@
|
|||||||
"snackbar_failed_load_audit": "आमंत्रण ऑडिट लोड करने में विफल",
|
"snackbar_failed_load_audit": "आमंत्रण ऑडिट लोड करने में विफल",
|
||||||
"snackbar_metadata_copied": "आमंत्रण मेटाडेटा कॉपी किया गया",
|
"snackbar_metadata_copied": "आमंत्रण मेटाडेटा कॉपी किया गया",
|
||||||
"snackbar_email_invites_success": "{गिनती} आमंत्रण बनाया गया। भेजा गया {भेजा गया}, असफल हुआ {असफल}। {डुप्लिकेट} डुप्लिकेट को छोड़ दिया गया।",
|
"snackbar_email_invites_success": "{गिनती} आमंत्रण बनाया गया। भेजा गया {भेजा गया}, असफल हुआ {असफल}। {डुप्लिकेट} डुप्लिकेट को छोड़ दिया गया।",
|
||||||
|
"select_at_least_one_student": "Select at least one existing student",
|
||||||
"presets": {
|
"presets": {
|
||||||
"one_time_24h": "एक बार (24 घंटे)",
|
"one_time_24h": "एक बार (24 घंटे)",
|
||||||
"multi_use_7d": "बहु-उपयोग (7 दिन)",
|
"multi_use_7d": "बहु-उपयोग (7 दिन)",
|
||||||
@@ -2492,13 +2504,22 @@
|
|||||||
"description": "प्रति एकीकरण एक ऑर्ग-स्कोप्ड कुंजी का उपयोग करें। यदि कोई कुंजी घुमाई जाती है या निरस्त की जाती है, तो क्लाइंट नया रहस्य कॉन्फ़िगर होने तक काम करना बंद कर देता है।",
|
"description": "प्रति एकीकरण एक ऑर्ग-स्कोप्ड कुंजी का उपयोग करें। यदि कोई कुंजी घुमाई जाती है या निरस्त की जाती है, तो क्लाइंट नया रहस्य कॉन्फ़िगर होने तक काम करना बंद कर देता है।",
|
||||||
"claude_code_description": "उत्पन्न ऑर्ग-स्कोप्ड कुंजी का उपयोग करके क्लाउड कोड के साथ `@classroomio/mcp` को पंजीकृत करता है।",
|
"claude_code_description": "उत्पन्न ऑर्ग-स्कोप्ड कुंजी का उपयोग करके क्लाउड कोड के साथ `@classroomio/mcp` को पंजीकृत करता है।",
|
||||||
"codex_description": "समान ऑर्ग-स्कोप्ड कुंजी के साथ क्लासरूमआईओ एमसीपी सर्वर को कोडेक्स में जोड़ता है।",
|
"codex_description": "समान ऑर्ग-स्कोप्ड कुंजी के साथ क्लासरूमआईओ एमसीपी सर्वर को कोडेक्स में जोड़ता है।",
|
||||||
"cursor_description": "इसे `~/.cursor/mcp.json` या अपने कार्यक्षेत्र MCP कॉन्फ़िगरेशन में पेस्ट करें।"
|
"cursor_description": "इसे `~/.cursor/mcp.json` या अपने कार्यक्षेत्र MCP कॉन्फ़िगरेशन में पेस्ट करें।",
|
||||||
|
"opencode_description": "इसे अपने प्रोजेक्ट रूट के `opencode.json` या `~/.config/opencode/opencode.json` में जोड़ें।"
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
"title": "एमसीपी कुंजी",
|
"title": "एमसीपी कुंजी",
|
||||||
"description": "जब कोई MCP क्लाइंट `@classroomio/mcp` के माध्यम से ClassroomIO को कॉल करता है तो ये कुंजियाँ वर्तमान संगठन को प्रमाणित करती हैं।",
|
"description": "जब कोई MCP क्लाइंट `@classroomio/mcp` के माध्यम से ClassroomIO को कॉल करता है तो ये कुंजियाँ वर्तमान संगठन को प्रमाणित करती हैं।",
|
||||||
"generate": "MCP कुंजी जनरेट करें",
|
"generate": "MCP कुंजी जनरेट करें",
|
||||||
"secret_once": "यह रहस्य एक बार दिखाया गया है। अब इसे अपने MCP क्लाइंट में कॉपी करें।"
|
"secret_once": "यह रहस्य एक बार दिखाया गया है। अब इसे अपने MCP क्लाइंट में कॉपी करें।",
|
||||||
|
"modal": {
|
||||||
|
"title": "Create MCP key",
|
||||||
|
"description": "Choose a name for this MCP key before it is generated.",
|
||||||
|
"name_label": "Key name",
|
||||||
|
"name_placeholder": "ClassroomIO OpenCode",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Create key"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
@@ -2507,7 +2528,7 @@
|
|||||||
"rotate": "घुमाएँ",
|
"rotate": "घुमाएँ",
|
||||||
"revoke": "निरस्त करें",
|
"revoke": "निरस्त करें",
|
||||||
"empty_title": "अभी तक कोई MCP कुंजी नहीं है",
|
"empty_title": "अभी तक कोई MCP कुंजी नहीं है",
|
||||||
"empty_description": "क्लाउड कोड, कोडेक्स, या कर्सर को कनेक्ट करने के लिए पहली ऑर्ग-स्कोप्ड एमसीपी कुंजी बनाएं।",
|
"empty_description": "क्लाउड कोड, कोडेक्स, कर्सर, या OpenCode को कनेक्ट करने के लिए पहली ऑर्ग-स्कोप्ड एमसीपी कुंजी बनाएं।",
|
||||||
"created_at_label": "बनाया गया:",
|
"created_at_label": "बनाया गया:",
|
||||||
"last_used_label": "अंतिम बार उपयोग किया गया:",
|
"last_used_label": "अंतिम बार उपयोग किया गया:",
|
||||||
"never_used": "कभी नहीं",
|
"never_used": "कभी नहीं",
|
||||||
@@ -2518,7 +2539,8 @@
|
|||||||
"clients": {
|
"clients": {
|
||||||
"claude_code": "क्लाउड कोड",
|
"claude_code": "क्लाउड कोड",
|
||||||
"codex": "कोडेक्स",
|
"codex": "कोडेक्स",
|
||||||
"cursor": "कर्सर"
|
"cursor": "कर्सर",
|
||||||
|
"opencode": "OpenCode"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"mcp": "एम.सी.पी",
|
"mcp": "एम.सी.पी",
|
||||||
|
|||||||
@@ -794,6 +794,17 @@
|
|||||||
"recipient_emails_placeholder": "[email protected], [email protected]",
|
"recipient_emails_placeholder": "[email protected], [email protected]",
|
||||||
"send_invite_emails": "Natychmiast wysyłaj e-maile z zaproszeniami",
|
"send_invite_emails": "Natychmiast wysyłaj e-maile z zaproszeniami",
|
||||||
"create_email_invites": "Utwórz zaproszenia e-mailowe",
|
"create_email_invites": "Utwórz zaproszenia e-mailowe",
|
||||||
|
"existing_students_title": "Add existing org students",
|
||||||
|
"existing_students_description": "Choose student members already in this organization and grant them direct access to this course.",
|
||||||
|
"existing_students_search": "Search organization students",
|
||||||
|
"loading_existing_students": "Loading organization students...",
|
||||||
|
"no_existing_students": "No organization students are available to add.",
|
||||||
|
"no_matching_existing_students": "No organization students match your search.",
|
||||||
|
"notify_existing_students": "Send course access email",
|
||||||
|
"grant_access": "Grant Access",
|
||||||
|
"invite_new_students_title": "Invite new students",
|
||||||
|
"invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.",
|
||||||
|
"invite_new_students": "Invite Students",
|
||||||
"existing_invites": "Istniejące zaproszenia",
|
"existing_invites": "Istniejące zaproszenia",
|
||||||
"refresh": "Odśwież",
|
"refresh": "Odśwież",
|
||||||
"loading_invites": "Ładowanie zaproszeń...",
|
"loading_invites": "Ładowanie zaproszeń...",
|
||||||
@@ -827,6 +838,7 @@
|
|||||||
"snackbar_failed_load_audit": "Nie udało się wczytać audytu zaproszeń",
|
"snackbar_failed_load_audit": "Nie udało się wczytać audytu zaproszeń",
|
||||||
"snackbar_metadata_copied": "Zaproś skopiowane metadane",
|
"snackbar_metadata_copied": "Zaproś skopiowane metadane",
|
||||||
"snackbar_email_invites_success": "Utworzono zaproszenia ({count}). Wysłano {wysłano}, nie udało się {nie udało się}. Pominięte duplikaty: {duplikaty}.",
|
"snackbar_email_invites_success": "Utworzono zaproszenia ({count}). Wysłano {wysłano}, nie udało się {nie udało się}. Pominięte duplikaty: {duplikaty}.",
|
||||||
|
"select_at_least_one_student": "Select at least one existing student",
|
||||||
"presets": {
|
"presets": {
|
||||||
"one_time_24h": "Jednorazowo (24h)",
|
"one_time_24h": "Jednorazowo (24h)",
|
||||||
"multi_use_7d": "Wielokrotnego użytku (7 dni)",
|
"multi_use_7d": "Wielokrotnego użytku (7 dni)",
|
||||||
@@ -2492,13 +2504,22 @@
|
|||||||
"description": "Użyj jednego klucza o zasięgu organizacji na integrację. Jeśli klucz zostanie obrócony lub unieważniony, klient przestaje działać do czasu skonfigurowania nowego klucza tajnego.",
|
"description": "Użyj jednego klucza o zasięgu organizacji na integrację. Jeśli klucz zostanie obrócony lub unieważniony, klient przestaje działać do czasu skonfigurowania nowego klucza tajnego.",
|
||||||
"claude_code_description": "Rejestruje `@classroomio/mcp` w Claude Code przy użyciu wygenerowanego klucza o zasięgu organizacji.",
|
"claude_code_description": "Rejestruje `@classroomio/mcp` w Claude Code przy użyciu wygenerowanego klucza o zasięgu organizacji.",
|
||||||
"codex_description": "Dodaje serwer ClassroomIO MCP do Codexu z tym samym kluczem o zasięgu organizacji.",
|
"codex_description": "Dodaje serwer ClassroomIO MCP do Codexu z tym samym kluczem o zasięgu organizacji.",
|
||||||
"cursor_description": "Wklej to do `~/.cursor/mcp.json` lub konfiguracji MCP w obszarze roboczym."
|
"cursor_description": "Wklej to do `~/.cursor/mcp.json` lub konfiguracji MCP w obszarze roboczym.",
|
||||||
|
"opencode_description": "Dodaj to do `opencode.json` w katalogu głównym projektu lub do `~/.config/opencode/opencode.json`."
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
"title": "Klucze MCP",
|
"title": "Klucze MCP",
|
||||||
"description": "Te klucze uwierzytelniają bieżącą organizację, gdy klient MCP wywołuje ClassroomIO poprzez `@classroomio/mcp`.",
|
"description": "Te klucze uwierzytelniają bieżącą organizację, gdy klient MCP wywołuje ClassroomIO poprzez `@classroomio/mcp`.",
|
||||||
"generate": "Wygeneruj klucz MCP",
|
"generate": "Wygeneruj klucz MCP",
|
||||||
"secret_once": "Sekret ten zostaje pokazany raz. Skopiuj go teraz do swojego klienta MCP."
|
"secret_once": "Sekret ten zostaje pokazany raz. Skopiuj go teraz do swojego klienta MCP.",
|
||||||
|
"modal": {
|
||||||
|
"title": "Create MCP key",
|
||||||
|
"description": "Choose a name for this MCP key before it is generated.",
|
||||||
|
"name_label": "Key name",
|
||||||
|
"name_placeholder": "ClassroomIO OpenCode",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Create key"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
@@ -2507,7 +2528,7 @@
|
|||||||
"rotate": "Obróć",
|
"rotate": "Obróć",
|
||||||
"revoke": "Odwołaj",
|
"revoke": "Odwołaj",
|
||||||
"empty_title": "Nie ma jeszcze kluczy MCP",
|
"empty_title": "Nie ma jeszcze kluczy MCP",
|
||||||
"empty_description": "Utwórz pierwszy klucz MCP o zasięgu organizacji, aby połączyć Claude Code, Codex lub Cursor.",
|
"empty_description": "Utwórz pierwszy klucz MCP o zasięgu organizacji, aby połączyć Claude Code, Codex, Cursor lub OpenCode.",
|
||||||
"created_at_label": "Utworzono:",
|
"created_at_label": "Utworzono:",
|
||||||
"last_used_label": "Ostatnio używane:",
|
"last_used_label": "Ostatnio używane:",
|
||||||
"never_used": "Nigdy",
|
"never_used": "Nigdy",
|
||||||
@@ -2518,7 +2539,8 @@
|
|||||||
"clients": {
|
"clients": {
|
||||||
"claude_code": "Claude'a Koda",
|
"claude_code": "Claude'a Koda",
|
||||||
"codex": "Kodeks",
|
"codex": "Kodeks",
|
||||||
"cursor": "Kursor"
|
"cursor": "Kursor",
|
||||||
|
"opencode": "OpenCode"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"mcp": "MCP",
|
"mcp": "MCP",
|
||||||
|
|||||||
@@ -794,6 +794,17 @@
|
|||||||
"recipient_emails_placeholder": "[email protected], [email protected]",
|
"recipient_emails_placeholder": "[email protected], [email protected]",
|
||||||
"send_invite_emails": "Envie e-mails de convite imediatamente",
|
"send_invite_emails": "Envie e-mails de convite imediatamente",
|
||||||
"create_email_invites": "Crie convites por e-mail",
|
"create_email_invites": "Crie convites por e-mail",
|
||||||
|
"existing_students_title": "Add existing org students",
|
||||||
|
"existing_students_description": "Choose student members already in this organization and grant them direct access to this course.",
|
||||||
|
"existing_students_search": "Search organization students",
|
||||||
|
"loading_existing_students": "Loading organization students...",
|
||||||
|
"no_existing_students": "No organization students are available to add.",
|
||||||
|
"no_matching_existing_students": "No organization students match your search.",
|
||||||
|
"notify_existing_students": "Send course access email",
|
||||||
|
"grant_access": "Grant Access",
|
||||||
|
"invite_new_students_title": "Invite new students",
|
||||||
|
"invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.",
|
||||||
|
"invite_new_students": "Invite Students",
|
||||||
"existing_invites": "Convites existentes",
|
"existing_invites": "Convites existentes",
|
||||||
"refresh": "Atualizar",
|
"refresh": "Atualizar",
|
||||||
"loading_invites": "Carregando convites...",
|
"loading_invites": "Carregando convites...",
|
||||||
@@ -827,6 +838,7 @@
|
|||||||
"snackbar_failed_load_audit": "Falha ao carregar auditoria de convite",
|
"snackbar_failed_load_audit": "Falha ao carregar auditoria de convite",
|
||||||
"snackbar_metadata_copied": "Metadados do convite copiados",
|
"snackbar_metadata_copied": "Metadados do convite copiados",
|
||||||
"snackbar_email_invites_success": "Foram criados {count} convite(s). Enviado {enviado}, falhou {falhou}. {duplicatas} duplicatas ignoradas.",
|
"snackbar_email_invites_success": "Foram criados {count} convite(s). Enviado {enviado}, falhou {falhou}. {duplicatas} duplicatas ignoradas.",
|
||||||
|
"select_at_least_one_student": "Select at least one existing student",
|
||||||
"presets": {
|
"presets": {
|
||||||
"one_time_24h": "Única (24h)",
|
"one_time_24h": "Única (24h)",
|
||||||
"multi_use_7d": "Multiuso (7 dias)",
|
"multi_use_7d": "Multiuso (7 dias)",
|
||||||
@@ -2492,13 +2504,22 @@
|
|||||||
"description": "Use uma chave com escopo organizacional por integração. Se uma chave for rotacionada ou revogada, o cliente para de funcionar até que o novo segredo seja configurado.",
|
"description": "Use uma chave com escopo organizacional por integração. Se uma chave for rotacionada ou revogada, o cliente para de funcionar até que o novo segredo seja configurado.",
|
||||||
"claude_code_description": "Registra `@classroomio/mcp` com Claude Code usando a chave gerada com escopo organizacional.",
|
"claude_code_description": "Registra `@classroomio/mcp` com Claude Code usando a chave gerada com escopo organizacional.",
|
||||||
"codex_description": "Adiciona o servidor ClassroomIO MCP ao Codex com a mesma chave com escopo organizacional.",
|
"codex_description": "Adiciona o servidor ClassroomIO MCP ao Codex com a mesma chave com escopo organizacional.",
|
||||||
"cursor_description": "Cole isso em `~/.cursor/mcp.json` ou na configuração do MCP do seu espaço de trabalho."
|
"cursor_description": "Cole isso em `~/.cursor/mcp.json` ou na configuração do MCP do seu espaço de trabalho.",
|
||||||
|
"opencode_description": "Adicione isso a `opencode.json` na raiz do projeto ou a `~/.config/opencode/opencode.json`."
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
"title": "Chaves MCP",
|
"title": "Chaves MCP",
|
||||||
"description": "Essas chaves autenticam a organização atual quando um cliente MCP chama ClassroomIO por meio de `@classroomio/mcp`.",
|
"description": "Essas chaves autenticam a organização atual quando um cliente MCP chama ClassroomIO por meio de `@classroomio/mcp`.",
|
||||||
"generate": "Gerar chave MCP",
|
"generate": "Gerar chave MCP",
|
||||||
"secret_once": "Este segredo é mostrado uma vez. Copie-o para o seu cliente MCP agora."
|
"secret_once": "Este segredo é mostrado uma vez. Copie-o para o seu cliente MCP agora.",
|
||||||
|
"modal": {
|
||||||
|
"title": "Create MCP key",
|
||||||
|
"description": "Choose a name for this MCP key before it is generated.",
|
||||||
|
"name_label": "Key name",
|
||||||
|
"name_placeholder": "ClassroomIO OpenCode",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Create key"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
@@ -2507,7 +2528,7 @@
|
|||||||
"rotate": "Girar",
|
"rotate": "Girar",
|
||||||
"revoke": "Revogar",
|
"revoke": "Revogar",
|
||||||
"empty_title": "Nenhuma chave MCP ainda",
|
"empty_title": "Nenhuma chave MCP ainda",
|
||||||
"empty_description": "Crie a primeira chave MCP com escopo organizacional para conectar Claude Code, Codex ou Cursor.",
|
"empty_description": "Crie a primeira chave MCP com escopo organizacional para conectar Claude Code, Codex, Cursor ou OpenCode.",
|
||||||
"created_at_label": "Criado:",
|
"created_at_label": "Criado:",
|
||||||
"last_used_label": "Usado pela última vez:",
|
"last_used_label": "Usado pela última vez:",
|
||||||
"never_used": "Nunca",
|
"never_used": "Nunca",
|
||||||
@@ -2518,7 +2539,8 @@
|
|||||||
"clients": {
|
"clients": {
|
||||||
"claude_code": "Código Claude",
|
"claude_code": "Código Claude",
|
||||||
"codex": "Códice",
|
"codex": "Códice",
|
||||||
"cursor": "Cursor"
|
"cursor": "Cursor",
|
||||||
|
"opencode": "OpenCode"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"mcp": "PCM",
|
"mcp": "PCM",
|
||||||
|
|||||||
@@ -794,6 +794,17 @@
|
|||||||
"recipient_emails_placeholder": "студент[email protected], студент[email protected]",
|
"recipient_emails_placeholder": "студент[email protected], студент[email protected]",
|
||||||
"send_invite_emails": "Отправляйте приглашения по электронной почте немедленно",
|
"send_invite_emails": "Отправляйте приглашения по электронной почте немедленно",
|
||||||
"create_email_invites": "Создание приглашений по электронной почте",
|
"create_email_invites": "Создание приглашений по электронной почте",
|
||||||
|
"existing_students_title": "Add existing org students",
|
||||||
|
"existing_students_description": "Choose student members already in this organization and grant them direct access to this course.",
|
||||||
|
"existing_students_search": "Search organization students",
|
||||||
|
"loading_existing_students": "Loading organization students...",
|
||||||
|
"no_existing_students": "No organization students are available to add.",
|
||||||
|
"no_matching_existing_students": "No organization students match your search.",
|
||||||
|
"notify_existing_students": "Send course access email",
|
||||||
|
"grant_access": "Grant Access",
|
||||||
|
"invite_new_students_title": "Invite new students",
|
||||||
|
"invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.",
|
||||||
|
"invite_new_students": "Invite Students",
|
||||||
"existing_invites": "Существующие приглашения",
|
"existing_invites": "Существующие приглашения",
|
||||||
"refresh": "Обновить",
|
"refresh": "Обновить",
|
||||||
"loading_invites": "Загрузка приглашений...",
|
"loading_invites": "Загрузка приглашений...",
|
||||||
@@ -827,6 +838,7 @@
|
|||||||
"snackbar_failed_load_audit": "Не удалось загрузить аудит приглашений.",
|
"snackbar_failed_load_audit": "Не удалось загрузить аудит приглашений.",
|
||||||
"snackbar_metadata_copied": "Метаданные приглашения скопированы.",
|
"snackbar_metadata_copied": "Метаданные приглашения скопированы.",
|
||||||
"snackbar_email_invites_success": "Создано приглашений: {count}. Отправлено {отправлено}, ошибка {failed}. Пропущено дубликатов: {duulates}.",
|
"snackbar_email_invites_success": "Создано приглашений: {count}. Отправлено {отправлено}, ошибка {failed}. Пропущено дубликатов: {duulates}.",
|
||||||
|
"select_at_least_one_student": "Select at least one existing student",
|
||||||
"presets": {
|
"presets": {
|
||||||
"one_time_24h": "Одноразовый (24 часа)",
|
"one_time_24h": "Одноразовый (24 часа)",
|
||||||
"multi_use_7d": "Многоразовое использование (7 дней)",
|
"multi_use_7d": "Многоразовое использование (7 дней)",
|
||||||
@@ -2492,13 +2504,22 @@
|
|||||||
"description": "Используйте один ключ на уровне организации для каждой интеграции. Если ключ заменен или отозван, клиент перестает работать до тех пор, пока не будет настроен новый секрет.",
|
"description": "Используйте один ключ на уровне организации для каждой интеграции. Если ключ заменен или отозван, клиент перестает работать до тех пор, пока не будет настроен новый секрет.",
|
||||||
"claude_code_description": "Регистрирует `@classroomio/mcp` в Claude Code, используя сгенерированный ключ на уровне организации.",
|
"claude_code_description": "Регистрирует `@classroomio/mcp` в Claude Code, используя сгенерированный ключ на уровне организации.",
|
||||||
"codex_description": "Добавляет сервер ClassroomIO MCP в Кодекс с тем же ключом на уровне организации.",
|
"codex_description": "Добавляет сервер ClassroomIO MCP в Кодекс с тем же ключом на уровне организации.",
|
||||||
"cursor_description": "Вставьте это в `~/.cursor/mcp.json` или в конфигурацию MCP вашей рабочей области."
|
"cursor_description": "Вставьте это в `~/.cursor/mcp.json` или в конфигурацию MCP вашей рабочей области.",
|
||||||
|
"opencode_description": "Добавьте это в `opencode.json` в корне проекта или в `~/.config/opencode/opencode.json`."
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
"title": "Ключи MCP",
|
"title": "Ключи MCP",
|
||||||
"description": "Эти ключи аутентифицируют текущую организацию, когда клиент MCP вызывает ClassroomIO через @classroomio/mcp.",
|
"description": "Эти ключи аутентифицируют текущую организацию, когда клиент MCP вызывает ClassroomIO через @classroomio/mcp.",
|
||||||
"generate": "Создать ключ MCP",
|
"generate": "Создать ключ MCP",
|
||||||
"secret_once": "Этот секрет раскрывается один раз. Скопируйте его в свой клиент MCP сейчас."
|
"secret_once": "Этот секрет раскрывается один раз. Скопируйте его в свой клиент MCP сейчас.",
|
||||||
|
"modal": {
|
||||||
|
"title": "Create MCP key",
|
||||||
|
"description": "Choose a name for this MCP key before it is generated.",
|
||||||
|
"name_label": "Key name",
|
||||||
|
"name_placeholder": "ClassroomIO OpenCode",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Create key"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
@@ -2507,7 +2528,7 @@
|
|||||||
"rotate": "Поворот",
|
"rotate": "Поворот",
|
||||||
"revoke": "Отозвать",
|
"revoke": "Отозвать",
|
||||||
"empty_title": "Ключей MCP пока нет",
|
"empty_title": "Ключей MCP пока нет",
|
||||||
"empty_description": "Создайте первый ключ MCP на уровне организации для подключения Claude Code, Codex или Cursor.",
|
"empty_description": "Создайте первый ключ MCP на уровне организации для подключения Claude Code, Codex, Cursor или OpenCode.",
|
||||||
"created_at_label": "Создано:",
|
"created_at_label": "Создано:",
|
||||||
"last_used_label": "Последнее использование:",
|
"last_used_label": "Последнее использование:",
|
||||||
"never_used": "Никогда",
|
"never_used": "Никогда",
|
||||||
@@ -2518,7 +2539,8 @@
|
|||||||
"clients": {
|
"clients": {
|
||||||
"claude_code": "Клод Код",
|
"claude_code": "Клод Код",
|
||||||
"codex": "Кодекс",
|
"codex": "Кодекс",
|
||||||
"cursor": "Курсор"
|
"cursor": "Курсор",
|
||||||
|
"opencode": "OpenCode"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"mcp": "МКП",
|
"mcp": "МКП",
|
||||||
|
|||||||
@@ -794,6 +794,17 @@
|
|||||||
"recipient_emails_placeholder": "sinh viê[email protected], sinh viê[email protected]",
|
"recipient_emails_placeholder": "sinh viê[email protected], sinh viê[email protected]",
|
||||||
"send_invite_emails": "Gửi email mời ngay lập tức",
|
"send_invite_emails": "Gửi email mời ngay lập tức",
|
||||||
"create_email_invites": "Tạo lời mời qua email",
|
"create_email_invites": "Tạo lời mời qua email",
|
||||||
|
"existing_students_title": "Add existing org students",
|
||||||
|
"existing_students_description": "Choose student members already in this organization and grant them direct access to this course.",
|
||||||
|
"existing_students_search": "Search organization students",
|
||||||
|
"loading_existing_students": "Loading organization students...",
|
||||||
|
"no_existing_students": "No organization students are available to add.",
|
||||||
|
"no_matching_existing_students": "No organization students match your search.",
|
||||||
|
"notify_existing_students": "Send course access email",
|
||||||
|
"grant_access": "Grant Access",
|
||||||
|
"invite_new_students_title": "Invite new students",
|
||||||
|
"invite_new_students_description": "Invite new people into your organization and pre-assign them to this course. They will create an account, accept the invite, and get access.",
|
||||||
|
"invite_new_students": "Invite Students",
|
||||||
"existing_invites": "Lời mời hiện có",
|
"existing_invites": "Lời mời hiện có",
|
||||||
"refresh": "Làm mới",
|
"refresh": "Làm mới",
|
||||||
"loading_invites": "Đang tải lời mời...",
|
"loading_invites": "Đang tải lời mời...",
|
||||||
@@ -827,6 +838,7 @@
|
|||||||
"snackbar_failed_load_audit": "Không tải được lời mời kiểm tra",
|
"snackbar_failed_load_audit": "Không tải được lời mời kiểm tra",
|
||||||
"snackbar_metadata_copied": "Đã sao chép siêu dữ liệu lời mời",
|
"snackbar_metadata_copied": "Đã sao chép siêu dữ liệu lời mời",
|
||||||
"snackbar_email_invites_success": "Đã tạo {count} lời mời. Đã gửi {đã gửi}, không thành công {không thành công}. Đã bỏ qua {trùng lặp} bản sao.",
|
"snackbar_email_invites_success": "Đã tạo {count} lời mời. Đã gửi {đã gửi}, không thành công {không thành công}. Đã bỏ qua {trùng lặp} bản sao.",
|
||||||
|
"select_at_least_one_student": "Select at least one existing student",
|
||||||
"presets": {
|
"presets": {
|
||||||
"one_time_24h": "Một lần (24h)",
|
"one_time_24h": "Một lần (24h)",
|
||||||
"multi_use_7d": "Sử dụng nhiều lần (7 ngày)",
|
"multi_use_7d": "Sử dụng nhiều lần (7 ngày)",
|
||||||
@@ -2492,13 +2504,22 @@
|
|||||||
"description": "Sử dụng một khóa trong phạm vi tổ chức cho mỗi lần tích hợp. Nếu một khóa bị xoay hoặc bị thu hồi, máy khách sẽ ngừng hoạt động cho đến khi bí mật mới được định cấu hình.",
|
"description": "Sử dụng một khóa trong phạm vi tổ chức cho mỗi lần tích hợp. Nếu một khóa bị xoay hoặc bị thu hồi, máy khách sẽ ngừng hoạt động cho đến khi bí mật mới được định cấu hình.",
|
||||||
"claude_code_description": "Đăng ký `@classroomio/mcp` với Mã Claude bằng cách sử dụng khóa trong phạm vi tổ chức được tạo.",
|
"claude_code_description": "Đăng ký `@classroomio/mcp` với Mã Claude bằng cách sử dụng khóa trong phạm vi tổ chức được tạo.",
|
||||||
"codex_description": "Thêm máy chủ ClassroomIO MCP vào Codex bằng cùng một khóa trong phạm vi tổ chức.",
|
"codex_description": "Thêm máy chủ ClassroomIO MCP vào Codex bằng cùng một khóa trong phạm vi tổ chức.",
|
||||||
"cursor_description": "Dán cái này vào `~/.cursor/mcp.json` hoặc cấu hình MCP không gian làm việc của bạn."
|
"cursor_description": "Dán cái này vào `~/.cursor/mcp.json` hoặc cấu hình MCP không gian làm việc của bạn.",
|
||||||
|
"opencode_description": "Thêm nội dung này vào `opencode.json` ở thư mục gốc dự án hoặc `~/.config/opencode/opencode.json`."
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
"title": "Khóa MCP",
|
"title": "Khóa MCP",
|
||||||
"description": "Các khóa này xác thực tổ chức hiện tại khi máy khách MCP gọi ClassroomIO thông qua `@classroomio/mcp`.",
|
"description": "Các khóa này xác thực tổ chức hiện tại khi máy khách MCP gọi ClassroomIO thông qua `@classroomio/mcp`.",
|
||||||
"generate": "Tạo khóa MCP",
|
"generate": "Tạo khóa MCP",
|
||||||
"secret_once": "Bí mật này được hiển thị một lần. Sao chép nó vào ứng dụng khách MCP của bạn ngay bây giờ."
|
"secret_once": "Bí mật này được hiển thị một lần. Sao chép nó vào ứng dụng khách MCP của bạn ngay bây giờ.",
|
||||||
|
"modal": {
|
||||||
|
"title": "Create MCP key",
|
||||||
|
"description": "Choose a name for this MCP key before it is generated.",
|
||||||
|
"name_label": "Key name",
|
||||||
|
"name_placeholder": "ClassroomIO OpenCode",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Create key"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
@@ -2507,7 +2528,7 @@
|
|||||||
"rotate": "Xoay",
|
"rotate": "Xoay",
|
||||||
"revoke": "Thu hồi",
|
"revoke": "Thu hồi",
|
||||||
"empty_title": "Chưa có khóa MCP",
|
"empty_title": "Chưa có khóa MCP",
|
||||||
"empty_description": "Tạo khóa MCP trong phạm vi tổ chức đầu tiên để kết nối Mã Claude, Codex hoặc Con trỏ.",
|
"empty_description": "Tạo khóa MCP trong phạm vi tổ chức đầu tiên để kết nối Mã Claude, Codex, Con trỏ hoặc OpenCode.",
|
||||||
"created_at_label": "Đã tạo:",
|
"created_at_label": "Đã tạo:",
|
||||||
"last_used_label": "Sử dụng lần cuối:",
|
"last_used_label": "Sử dụng lần cuối:",
|
||||||
"never_used": "không bao giờ",
|
"never_used": "không bao giờ",
|
||||||
@@ -2518,7 +2539,8 @@
|
|||||||
"clients": {
|
"clients": {
|
||||||
"claude_code": "Mã Claude",
|
"claude_code": "Mã Claude",
|
||||||
"codex": "Codex",
|
"codex": "Codex",
|
||||||
"cursor": "Con trỏ"
|
"cursor": "Con trỏ",
|
||||||
|
"opencode": "OpenCode"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"mcp": "MCP",
|
"mcp": "MCP",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: MCP
|
title: MCP
|
||||||
description: Connect Claude Code, Codex, Cursor, and other MCP clients to ClassroomIO for draft-based course authoring.
|
description: Connect Claude Code, Codex, Cursor, OpenCode, and other MCP clients to ClassroomIO for draft-based course authoring.
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Callout } from 'fumadocs-ui/components/callout';
|
import { Callout } from 'fumadocs-ui/components/callout';
|
||||||
@@ -17,7 +17,7 @@ ClassroomIO API.
|
|||||||
Teacher / Admin
|
Teacher / Admin
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
Claude Code / Codex / Cursor / other MCP client
|
Claude Code / Codex / Cursor / OpenCode / other MCP client
|
||||||
|
|
|
|
||||||
| 1. Reads prompt, PDF, or existing course goal
|
| 1. Reads prompt, PDF, or existing course goal
|
||||||
| 2. Produces or edits normalized course JSON
|
| 2. Produces or edits normalized course JSON
|
||||||
@@ -109,6 +109,25 @@ codex mcp add classroomio \
|
|||||||
-- npx -y @classroomio/mcp
|
-- npx -y @classroomio/mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### OpenCode
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"classroomio": {
|
||||||
|
"type": "local",
|
||||||
|
"command": ["npx", "-y", "@classroomio/mcp"],
|
||||||
|
"enabled": true,
|
||||||
|
"environment": {
|
||||||
|
"CLASSROOMIO_API_URL": "https://api.classroomio.com",
|
||||||
|
"CLASSROOMIO_API_KEY": "<cio_mcp_key>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Cursor
|
### Cursor
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
+20
-1
@@ -10,7 +10,7 @@ The package does not parse PDFs or generate course structures. The agent does th
|
|||||||
Teacher / Admin
|
Teacher / Admin
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
Claude Code / Codex / Cursor / other MCP client
|
Claude Code / Codex / Cursor / OpenCode / other MCP client
|
||||||
|
|
|
|
||||||
| 1. Reads prompt, PDF, or existing course goal
|
| 1. Reads prompt, PDF, or existing course goal
|
||||||
| 2. Produces or edits normalized course JSON
|
| 2. Produces or edits normalized course JSON
|
||||||
@@ -123,6 +123,25 @@ codex mcp add classroomio \
|
|||||||
-- npx -y @classroomio/mcp
|
-- npx -y @classroomio/mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### OpenCode
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"classroomio": {
|
||||||
|
"type": "local",
|
||||||
|
"command": ["npx", "-y", "@classroomio/mcp"],
|
||||||
|
"enabled": true,
|
||||||
|
"environment": {
|
||||||
|
"CLASSROOMIO_API_URL": "https://api.classroomio.com",
|
||||||
|
"CLASSROOMIO_API_KEY": "<cio_mcp_key>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Cursor
|
### Cursor
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@classroomio/mcp",
|
"name": "@classroomio/mcp",
|
||||||
"version": "0.0.8",
|
"version": "0.0.9",
|
||||||
"private": false,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
|||||||
const ZConfig = z.object({
|
const ZConfig = z.object({
|
||||||
CLASSROOMIO_API_URL: z.url().default('https://api.classroomio.com'),
|
CLASSROOMIO_API_URL: z.url().default('https://api.classroomio.com'),
|
||||||
CLASSROOMIO_API_KEY: z.string().min(1),
|
CLASSROOMIO_API_KEY: z.string().min(1),
|
||||||
CLASSROOMIO_USER_AGENT: z.string().min(1).default('classroomio-mcp/0.0.1')
|
CLASSROOMIO_USER_AGENT: z.string().min(1).default('classroomio-mcp/0.0.9')
|
||||||
});
|
});
|
||||||
|
|
||||||
export type McpServerConfig = z.infer<typeof ZConfig>;
|
export type McpServerConfig = z.infer<typeof ZConfig>;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
import { ZCourseMetadata } from '../course/course';
|
import { ZCourseMetadata } from '../course/course';
|
||||||
|
import { ZExerciseQuestionTypeId } from '../exercise/exercise';
|
||||||
|
|
||||||
const ZSupportedLocale = z.enum(['en', 'hi', 'fr', 'pt', 'de', 'vi', 'ru', 'es', 'pl', 'da']);
|
const ZSupportedLocale = z.enum(['en', 'hi', 'fr', 'pt', 'de', 'vi', 'ru', 'es', 'pl', 'da']);
|
||||||
const LESSON_BODY_HTML_DESCRIPTION =
|
const LESSON_BODY_HTML_DESCRIPTION =
|
||||||
@@ -54,6 +55,35 @@ export const ZCourseImportDraftLessonLanguage = z.object({
|
|||||||
});
|
});
|
||||||
export type TCourseImportDraftLessonLanguage = z.infer<typeof ZCourseImportDraftLessonLanguage>;
|
export type TCourseImportDraftLessonLanguage = z.infer<typeof ZCourseImportDraftLessonLanguage>;
|
||||||
|
|
||||||
|
export const ZCourseImportDraftExerciseOption = z.object({
|
||||||
|
label: z.string().min(1),
|
||||||
|
isCorrect: z.boolean(),
|
||||||
|
settings: z.record(z.string(), z.unknown()).optional()
|
||||||
|
});
|
||||||
|
export type TCourseImportDraftExerciseOption = z.infer<typeof ZCourseImportDraftExerciseOption>;
|
||||||
|
|
||||||
|
export const ZCourseImportDraftExerciseQuestion = z.object({
|
||||||
|
question: z.string().min(1),
|
||||||
|
questionTypeId: ZExerciseQuestionTypeId.optional(),
|
||||||
|
points: z.number().int().min(0).optional(),
|
||||||
|
order: z.number().int().min(0).optional(),
|
||||||
|
settings: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
options: z.array(ZCourseImportDraftExerciseOption).optional()
|
||||||
|
});
|
||||||
|
export type TCourseImportDraftExerciseQuestion = z.infer<typeof ZCourseImportDraftExerciseQuestion>;
|
||||||
|
|
||||||
|
export const ZCourseImportDraftExercise = z.object({
|
||||||
|
externalId: z.string().min(1),
|
||||||
|
lessonExternalId: z.string().min(1).optional(),
|
||||||
|
sectionExternalId: z.string().min(1).optional(),
|
||||||
|
title: z.string().min(1),
|
||||||
|
description: z.string().optional(),
|
||||||
|
order: z.number().int().min(0).optional(),
|
||||||
|
dueBy: z.string().optional(),
|
||||||
|
questions: z.array(ZCourseImportDraftExerciseQuestion).optional()
|
||||||
|
});
|
||||||
|
export type TCourseImportDraftExercise = z.infer<typeof ZCourseImportDraftExercise>;
|
||||||
|
|
||||||
export const ZCourseImportDraftPayload = z
|
export const ZCourseImportDraftPayload = z
|
||||||
.object({
|
.object({
|
||||||
course: ZCourseImportDraftCourse,
|
course: ZCourseImportDraftCourse,
|
||||||
@@ -61,7 +91,7 @@ export const ZCourseImportDraftPayload = z
|
|||||||
sections: z.array(ZCourseImportDraftSection).min(1),
|
sections: z.array(ZCourseImportDraftSection).min(1),
|
||||||
lessons: z.array(ZCourseImportDraftLesson).min(1),
|
lessons: z.array(ZCourseImportDraftLesson).min(1),
|
||||||
lessonLanguages: z.array(ZCourseImportDraftLessonLanguage).min(1),
|
lessonLanguages: z.array(ZCourseImportDraftLessonLanguage).min(1),
|
||||||
exercises: z.array(z.record(z.string(), z.unknown())).optional(),
|
exercises: z.array(ZCourseImportDraftExercise).optional(),
|
||||||
sourceReferences: z.array(ZCourseImportSourceReference).optional(),
|
sourceReferences: z.array(ZCourseImportSourceReference).optional(),
|
||||||
warnings: z.array(ZCourseImportWarning).default([])
|
warnings: z.array(ZCourseImportWarning).default([])
|
||||||
})
|
})
|
||||||
@@ -120,6 +150,34 @@ export const ZCourseImportDraftPayload = z
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exerciseIds = new Set<string>();
|
||||||
|
value.exercises?.forEach((exercise, index) => {
|
||||||
|
if (exerciseIds.has(exercise.externalId)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ['exercises', index, 'externalId'],
|
||||||
|
message: 'Exercise externalId must be unique'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exerciseIds.add(exercise.externalId);
|
||||||
|
|
||||||
|
if (exercise.lessonExternalId && !lessonIds.has(exercise.lessonExternalId)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ['exercises', index, 'lessonExternalId'],
|
||||||
|
message: 'Exercise lessonExternalId must reference an existing lesson'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exercise.sectionExternalId && !sectionIds.has(exercise.sectionExternalId)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ['exercises', index, 'sectionExternalId'],
|
||||||
|
message: 'Exercise sectionExternalId must reference an existing section'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
export type TCourseImportDraftPayload = z.infer<typeof ZCourseImportDraftPayload>;
|
export type TCourseImportDraftPayload = z.infer<typeof ZCourseImportDraftPayload>;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,24 @@ import * as z from 'zod';
|
|||||||
|
|
||||||
import { QUESTION_TYPE } from '../constants';
|
import { QUESTION_TYPE } from '../constants';
|
||||||
|
|
||||||
|
const EXERCISE_QUESTION_TYPE_ID_LITERALS = [
|
||||||
|
z.literal(QUESTION_TYPE.RADIO),
|
||||||
|
z.literal(QUESTION_TYPE.CHECKBOX),
|
||||||
|
z.literal(QUESTION_TYPE.TEXTAREA),
|
||||||
|
z.literal(QUESTION_TYPE.TRUE_FALSE),
|
||||||
|
z.literal(QUESTION_TYPE.SHORT_ANSWER),
|
||||||
|
z.literal(QUESTION_TYPE.NUMERIC),
|
||||||
|
z.literal(QUESTION_TYPE.FILL_BLANK),
|
||||||
|
z.literal(QUESTION_TYPE.FILE_UPLOAD),
|
||||||
|
z.literal(QUESTION_TYPE.MATCHING),
|
||||||
|
z.literal(QUESTION_TYPE.ORDERING),
|
||||||
|
z.literal(QUESTION_TYPE.HOTSPOT),
|
||||||
|
z.literal(QUESTION_TYPE.LINK)
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const ZExerciseQuestionTypeId = z.union(EXERCISE_QUESTION_TYPE_ID_LITERALS);
|
||||||
|
export type TExerciseQuestionTypeId = z.infer<typeof ZExerciseQuestionTypeId>;
|
||||||
|
|
||||||
const QUESTION_VALIDATION_RULES: Record<
|
const QUESTION_VALIDATION_RULES: Record<
|
||||||
number,
|
number,
|
||||||
Array<(question: { options?: Array<{ isCorrect: boolean }> }) => string | null>
|
Array<(question: { options?: Array<{ isCorrect: boolean }> }) => string | null>
|
||||||
@@ -61,7 +79,7 @@ const QUESTION_VALIDATION_RULES: Record<
|
|||||||
const ZExerciseUpdateQuestionBase = z.object({
|
const ZExerciseUpdateQuestionBase = z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
question: z.string().min(1),
|
question: z.string().min(1),
|
||||||
questionTypeId: z.number().int().min(1).optional(), // Added to support question type updates
|
questionTypeId: ZExerciseQuestionTypeId.optional(),
|
||||||
points: z.number().int().min(0).optional(),
|
points: z.number().int().min(0).optional(),
|
||||||
order: z.number().int().min(0).optional(),
|
order: z.number().int().min(0).optional(),
|
||||||
settings: z.record(z.string(), z.unknown()).optional(),
|
settings: z.record(z.string(), z.unknown()).optional(),
|
||||||
@@ -98,6 +116,25 @@ function validateQuestionOptions(question: z.infer<typeof ZExerciseUpdateQuestio
|
|||||||
// Final schema with validation refinement
|
// Final schema with validation refinement
|
||||||
const ZExerciseUpdateQuestion = ZExerciseUpdateQuestionBase.superRefine(validateQuestionOptions);
|
const ZExerciseUpdateQuestion = ZExerciseUpdateQuestionBase.superRefine(validateQuestionOptions);
|
||||||
|
|
||||||
|
const ZExerciseCreateQuestionBase = z.object({
|
||||||
|
question: z.string().min(1),
|
||||||
|
questionTypeId: ZExerciseQuestionTypeId.optional(),
|
||||||
|
points: z.number().int().min(0).optional(),
|
||||||
|
order: z.number().int().min(0).optional(),
|
||||||
|
settings: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
options: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string().min(1),
|
||||||
|
isCorrect: z.boolean(),
|
||||||
|
settings: z.record(z.string(), z.unknown()).optional()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const ZExerciseCreateQuestion = ZExerciseCreateQuestionBase.superRefine(validateQuestionOptions);
|
||||||
|
|
||||||
export const ZExerciseCreate = z.object({
|
export const ZExerciseCreate = z.object({
|
||||||
title: z.string().min(1),
|
title: z.string().min(1),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
@@ -106,25 +143,7 @@ export const ZExerciseCreate = z.object({
|
|||||||
order: z.number().int().optional(),
|
order: z.number().int().optional(),
|
||||||
courseId: z.string().min(1),
|
courseId: z.string().min(1),
|
||||||
dueBy: z.string().optional(),
|
dueBy: z.string().optional(),
|
||||||
questions: z
|
questions: z.array(ZExerciseCreateQuestion).optional()
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
question: z.string().min(1),
|
|
||||||
questionTypeId: z.number().int().min(1).optional(),
|
|
||||||
points: z.number().int().min(0).optional(),
|
|
||||||
settings: z.record(z.string(), z.unknown()).optional(),
|
|
||||||
options: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
label: z.string().min(1),
|
|
||||||
isCorrect: z.boolean(),
|
|
||||||
settings: z.record(z.string(), z.unknown()).optional()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.min(2)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional()
|
|
||||||
});
|
});
|
||||||
export type TExerciseCreate = z.infer<typeof ZExerciseCreate>;
|
export type TExerciseCreate = z.infer<typeof ZExerciseCreate>;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user