feat: implement automation key management with middleware and routes for organization API keys, invite improvements
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import type {
|
||||
AutomationKey,
|
||||
AutomationKeyType,
|
||||
CreateAutomationKeyRequest,
|
||||
ListAutomationKeysRequest,
|
||||
RevokeAutomationKeyRequest,
|
||||
RotateAutomationKeyRequest
|
||||
} from '$features/automation/utils/types';
|
||||
import { getDefaultAutomationKeyLabel } from '$features/automation/utils/automation-utils';
|
||||
import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api';
|
||||
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
|
||||
class AutomationApi extends BaseApiWithErrors {
|
||||
generatedSecret = $state<string | null>(null);
|
||||
isFetched = $state(false);
|
||||
keys = $state<AutomationKey[]>([]);
|
||||
|
||||
async listKeys(type?: AutomationKeyType) {
|
||||
return this.execute<ListAutomationKeysRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.organization.automation.keys.$get({
|
||||
query: type ? { type } : {}
|
||||
}),
|
||||
logContext: 'fetching automation keys',
|
||||
onSuccess: (response) => {
|
||||
this.keys = response.data;
|
||||
this.isFetched = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async createKey(type: AutomationKeyType, label?: string) {
|
||||
return this.execute<CreateAutomationKeyRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.organization.automation.keys.$post({
|
||||
json: {
|
||||
type,
|
||||
label: label?.trim() || getDefaultAutomationKeyLabel(type)
|
||||
}
|
||||
}),
|
||||
logContext: 'creating automation key',
|
||||
onSuccess: (response) => {
|
||||
this.generatedSecret = response.data.secret;
|
||||
this.keys = [response.data.key, ...this.keys];
|
||||
snackbar.success('snackbar.automation.created');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async revokeKey(keyId: string) {
|
||||
return this.execute<RevokeAutomationKeyRequest>({
|
||||
requestFn: () => classroomio.organization.automation.keys[':keyId'].$delete({ param: { keyId } }),
|
||||
logContext: 'revoking automation key',
|
||||
onSuccess: (response) => {
|
||||
this.keys = this.keys.map((key) => (key.id === response.data.id ? response.data : key));
|
||||
snackbar.success('snackbar.automation.revoked');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async rotateKey(keyId: string) {
|
||||
return this.execute<RotateAutomationKeyRequest>({
|
||||
requestFn: () => classroomio.organization.automation.keys[':keyId'].rotate.$post({ param: { keyId } }),
|
||||
logContext: 'rotating automation key',
|
||||
onSuccess: (response) => {
|
||||
this.generatedSecret = response.data.secret;
|
||||
this.keys = this.keys.map((key) => (key.id === response.data.key.id ? response.data.key : key));
|
||||
snackbar.success('snackbar.automation.rotated');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearGeneratedSecret() {
|
||||
this.generatedSecret = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const automationApi = new AutomationApi();
|
||||
@@ -0,0 +1,269 @@
|
||||
<script lang="ts">
|
||||
import { automationApi } from '$features/automation/api/automation.svelte';
|
||||
import {
|
||||
getClaudeCodeSnippet,
|
||||
getCodexSnippet,
|
||||
getCursorSnippet,
|
||||
getMaskedAutomationSecret
|
||||
} from '$features/automation/utils/automation-utils';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Alert from '@cio/ui/base/alert';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Card from '@cio/ui/base/card';
|
||||
import * as Code from '@cio/ui/custom/code';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import * as Tabs from '@cio/ui/base/tabs';
|
||||
import KeyIcon from '@lucide/svelte/icons/key';
|
||||
import SparklesIcon from '@lucide/svelte/icons/sparkles';
|
||||
import WandSparklesIcon from '@lucide/svelte/icons/wand-sparkles';
|
||||
import BracesIcon from '@lucide/svelte/icons/braces';
|
||||
|
||||
let activeTab = $state('mcp');
|
||||
let activeSetupTab = $state('claude-code');
|
||||
let isRevokingKeyId = $state<string | null>(null);
|
||||
let isRotatingKeyId = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (!$isOrgAdmin || automationApi.isFetched) return;
|
||||
|
||||
automationApi.listKeys('mcp');
|
||||
});
|
||||
|
||||
async function onGenerateKey() {
|
||||
await automationApi.createKey('mcp');
|
||||
}
|
||||
|
||||
async function onRevokeKey(keyId: string) {
|
||||
if (!confirm(t.get('automation.keys.revoke_confirm'))) return;
|
||||
|
||||
isRevokingKeyId = keyId;
|
||||
await automationApi.revokeKey(keyId);
|
||||
isRevokingKeyId = null;
|
||||
}
|
||||
|
||||
async function onRotateKey(keyId: string) {
|
||||
if (!confirm(t.get('automation.keys.rotate_confirm'))) return;
|
||||
|
||||
isRotatingKeyId = keyId;
|
||||
await automationApi.rotateKey(keyId);
|
||||
isRotatingKeyId = null;
|
||||
}
|
||||
|
||||
const mcpSecret = $derived(automationApi.generatedSecret);
|
||||
const mcpKeys = $derived(automationApi.keys.filter((key) => key.type === 'mcp'));
|
||||
const claudeCodeSnippet = $derived(getClaudeCodeSnippet(mcpSecret));
|
||||
const codexSnippet = $derived(getCodexSnippet(mcpSecret));
|
||||
const cursorSnippet = $derived(getCursorSnippet(mcpSecret));
|
||||
</script>
|
||||
|
||||
<Field.Group class="w-full max-w-5xl! space-y-8 px-2">
|
||||
{#if !$isOrgAdmin}
|
||||
<Alert.Callout
|
||||
variant="information"
|
||||
title={$t('automation.permissions.disabled_title')}
|
||||
description={$t('automation.permissions.disabled_description')}
|
||||
class="w-full"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Tabs.Root bind:value={activeTab} class="w-full">
|
||||
<Tabs.List class="grid w-full grid-cols-3">
|
||||
<Tabs.Trigger value="mcp">{$t('automation.tabs.mcp')}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="zapier" disabled>
|
||||
<span>{$t('automation.tabs.zapier')}</span>
|
||||
<Badge variant="outline" class="text-[10px]!">{$t('automation.coming_soon')}</Badge>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="api" disabled>
|
||||
<span>{$t('automation.tabs.api')}</span>
|
||||
<Badge variant="outline" class="text-[10px]!">{$t('automation.coming_soon')}</Badge>
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="mcp" class="space-y-8">
|
||||
<Field.Set>
|
||||
<Field.Legend class="flex items-center gap-2">
|
||||
<KeyIcon class="size-5" />
|
||||
{$t('automation.mcp.keys.title')}
|
||||
</Field.Legend>
|
||||
<Field.Description>{$t('automation.mcp.keys.description')}</Field.Description>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
onclick={onGenerateKey}
|
||||
variant="secondary"
|
||||
loading={automationApi.isLoading}
|
||||
disabled={!$isOrgAdmin || automationApi.isLoading}
|
||||
>
|
||||
{$t('automation.mcp.keys.generate')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if automationApi.generatedSecret}
|
||||
<div class="mt-2 rounded-lg border border-amber-200 bg-amber-50 p-4">
|
||||
<p class="ui:text-amber-800 text-sm font-medium">{$t('automation.mcp.keys.secret_once')}</p>
|
||||
<div class="mt-3">
|
||||
<Code.Root
|
||||
code={automationApi.generatedSecret}
|
||||
lang="bash"
|
||||
hideLines={true}
|
||||
class="ui:break-all ui:text-xs"
|
||||
>
|
||||
<Code.CopyButton />
|
||||
</Code.Root>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $isOrgAdmin}
|
||||
{#if mcpKeys.length > 0}
|
||||
<div class="mt-6 grid gap-4 md:grid-cols-2">
|
||||
{#each mcpKeys as key (key.id)}
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<Card.Title>{key.label}</Card.Title>
|
||||
<Card.Description>{getMaskedAutomationSecret(key.secretPrefix)}</Card.Description>
|
||||
</div>
|
||||
<Badge variant={key.revokedAt ? 'secondary' : 'default'}>
|
||||
{key.revokedAt ? $t('automation.keys.revoked') : $t('automation.keys.active')}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-2 text-sm">
|
||||
<p>
|
||||
<span class="font-medium">{$t('automation.keys.created_at_label')}</span>
|
||||
{new Date(key.createdAt).toLocaleString()}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-medium">{$t('automation.keys.last_used_label')}</span>
|
||||
{key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleString() : $t('automation.keys.never_used')}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-medium">{$t('automation.keys.scopes_label')}</span>
|
||||
{key.scopes.join(', ')}
|
||||
</p>
|
||||
</Card.Content>
|
||||
<Card.Footer class="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={() => onRotateKey(key.id)}
|
||||
loading={isRotatingKeyId === key.id}
|
||||
disabled={!$isOrgAdmin || Boolean(key.revokedAt)}
|
||||
>
|
||||
{$t('automation.keys.rotate')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onclick={() => onRevokeKey(key.id)}
|
||||
loading={isRevokingKeyId === key.id}
|
||||
disabled={!$isOrgAdmin || Boolean(key.revokedAt)}
|
||||
>
|
||||
{$t('automation.keys.revoke')}
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Alert.Callout
|
||||
variant="information"
|
||||
title={$t('automation.keys.empty_title')}
|
||||
description={$t('automation.keys.empty_description')}
|
||||
class="mt-6 w-full"
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</Field.Set>
|
||||
|
||||
<Field.Separator />
|
||||
|
||||
<Field.Set>
|
||||
<Field.Legend class="flex items-center gap-2">
|
||||
<SparklesIcon class="size-5" />
|
||||
{$t('automation.mcp.setup.title')}
|
||||
</Field.Legend>
|
||||
<Field.Description>{$t('automation.mcp.setup.description')}</Field.Description>
|
||||
|
||||
<Tabs.Root bind:value={activeSetupTab} class="w-full">
|
||||
<Tabs.List class="inline-flex w-auto">
|
||||
<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="cursor">{$t('automation.clients.cursor')}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="claude-code" class="mt-4">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{$t('automation.clients.claude_code')}</Card.Title>
|
||||
<Card.Description>{$t('automation.mcp.setup.claude_code_description')}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<Code.FileName filename="terminal" />
|
||||
<Code.Overflow>
|
||||
<Code.Root code={claudeCodeSnippet} lang="bash" class="ui:w-full ui:border-none">
|
||||
<Code.CopyButton />
|
||||
</Code.Root>
|
||||
</Code.Overflow>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="codex" class="mt-4">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{$t('automation.clients.codex')}</Card.Title>
|
||||
<Card.Description>{$t('automation.mcp.setup.codex_description')}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<Code.FileName filename="terminal" />
|
||||
<Code.Overflow>
|
||||
<Code.Root code={codexSnippet} lang="bash" class="ui:w-full ui:border-none">
|
||||
<Code.CopyButton />
|
||||
</Code.Root>
|
||||
</Code.Overflow>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="cursor" class="mt-4">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{$t('automation.clients.cursor')}</Card.Title>
|
||||
<Card.Description>{$t('automation.mcp.setup.cursor_description')}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<Code.FileName filename="mcp.json" />
|
||||
<Code.Overflow>
|
||||
<Code.Root code={cursorSnippet} lang="json" class="ui:w-full ui:border-none">
|
||||
<Code.CopyButton />
|
||||
</Code.Root>
|
||||
</Code.Overflow>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</Field.Set>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="zapier" class="mt-6">
|
||||
<Alert.Callout
|
||||
variant="information"
|
||||
title={$t('automation.tabs.zapier')}
|
||||
description={$t('automation.coming_soon')}
|
||||
class="w-full"
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="api" class="mt-6">
|
||||
<Alert.Callout
|
||||
variant="information"
|
||||
title={$t('automation.tabs.api')}
|
||||
description={$t('automation.coming_soon')}
|
||||
class="w-full"
|
||||
/>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</Field.Group>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as AutomationPage } from './automation.svelte';
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { AutomationKeyType } from './types';
|
||||
|
||||
function getServerPath() {
|
||||
return 'npx -y @classroomio/mcp';
|
||||
}
|
||||
|
||||
export function getAutomationSetupSecret(secret: string | null) {
|
||||
return secret ?? '<paste-your-mcp-key>';
|
||||
}
|
||||
|
||||
export function getClaudeCodeSnippet(secret: string | null) {
|
||||
const apiKey = getAutomationSetupSecret(secret);
|
||||
|
||||
return `claude mcp add-json classroomio '{
|
||||
"command": "npx",
|
||||
"args": ["-y", "@classroomio/mcp"],
|
||||
"env": {
|
||||
"CLASSROOMIO_API_URL": "https://api.classroomio.com",
|
||||
"CLASSROOMIO_API_KEY": "${apiKey}"
|
||||
}
|
||||
}'`;
|
||||
}
|
||||
|
||||
export function getCodexSnippet(secret: string | null) {
|
||||
const apiKey = getAutomationSetupSecret(secret);
|
||||
|
||||
return `codex mcp add classroomio \\
|
||||
--env CLASSROOMIO_API_URL=https://api.classroomio.com \\
|
||||
--env CLASSROOMIO_API_KEY=${apiKey} \\
|
||||
-- npx -y @classroomio/mcp`;
|
||||
}
|
||||
|
||||
export function getCursorSnippet(secret: string | null) {
|
||||
const apiKey = getAutomationSetupSecret(secret);
|
||||
|
||||
return `{
|
||||
"mcpServers": {
|
||||
"classroomio": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@classroomio/mcp"],
|
||||
"env": {
|
||||
"CLASSROOMIO_API_URL": "https://api.classroomio.com",
|
||||
"CLASSROOMIO_API_KEY": "${apiKey}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
export function getDefaultAutomationKeyLabel(type: AutomationKeyType) {
|
||||
switch (type) {
|
||||
case 'mcp':
|
||||
return 'ClassroomIO MCP';
|
||||
case 'api':
|
||||
return 'ClassroomIO API';
|
||||
case 'zapier':
|
||||
return 'ClassroomIO Zapier';
|
||||
}
|
||||
}
|
||||
|
||||
export function getAutomationKeyTypeLabel(type: AutomationKeyType) {
|
||||
switch (type) {
|
||||
case 'mcp':
|
||||
return 'MCP';
|
||||
case 'api':
|
||||
return 'API';
|
||||
case 'zapier':
|
||||
return 'Zapier';
|
||||
}
|
||||
}
|
||||
|
||||
export function getMaskedAutomationSecret(prefix: string) {
|
||||
return `${prefix}...`;
|
||||
}
|
||||
|
||||
export function getCopyableServerCommand() {
|
||||
return getServerPath();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { classroomio, type InferResponseType } from '$lib/utils/services/api';
|
||||
|
||||
export type ListAutomationKeysRequest = (typeof classroomio.organization)['automation']['keys']['$get'];
|
||||
export type CreateAutomationKeyRequest = (typeof classroomio.organization)['automation']['keys']['$post'];
|
||||
export type RevokeAutomationKeyRequest = (typeof classroomio.organization)['automation']['keys'][':keyId']['$delete'];
|
||||
export type RotateAutomationKeyRequest =
|
||||
(typeof classroomio.organization)['automation']['keys'][':keyId']['rotate']['$post'];
|
||||
|
||||
export type ListAutomationKeysSuccess = Extract<InferResponseType<ListAutomationKeysRequest>, { success: true }>;
|
||||
export type CreateAutomationKeySuccess = Extract<InferResponseType<CreateAutomationKeyRequest>, { success: true }>;
|
||||
export type RevokeAutomationKeySuccess = Extract<InferResponseType<RevokeAutomationKeyRequest>, { success: true }>;
|
||||
export type RotateAutomationKeySuccess = Extract<InferResponseType<RotateAutomationKeyRequest>, { success: true }>;
|
||||
|
||||
export type AutomationKey = ListAutomationKeysSuccess['data'][number];
|
||||
export type AutomationKeyType = AutomationKey['type'];
|
||||
@@ -14,7 +14,7 @@ import { currentOrg as currentOrgStore } from '$lib/utils/store/org';
|
||||
import type { TCreateCommunityQuestion } from '@cio/utils/validation/community';
|
||||
import { coursesApi } from '$features/course/api/courses.svelte';
|
||||
import { currentCommunityQuestion } from '../utils/store';
|
||||
import generateSlug from '$lib/utils/functions/generateSlug';
|
||||
import { generateSlug } from '@cio/utils/functions';
|
||||
import { get } from 'svelte/store';
|
||||
import { basePath, isStudentExperience } from '$lib/utils/store/app';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -121,7 +121,7 @@ class CommunityApi extends BaseApiWithErrors {
|
||||
return;
|
||||
}
|
||||
|
||||
const slug = generateSlug(fields.title);
|
||||
const slug = generateSlug(fields.title, { appendTimestamp: true });
|
||||
const currentOrg = get(currentOrgStore);
|
||||
const profile = get(profileStore);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { CloneCourseRequest } from '../utils/types';
|
||||
import type { TCourseClone } from '@cio/utils/validation/course';
|
||||
import { ZCourseClone } from '@cio/utils/validation/course';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import generateSlug from '$lib/utils/functions/generateSlug';
|
||||
import { generateSlug } from '@cio/utils/functions';
|
||||
import { get } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { mapZodErrorsToTranslations } from '$lib/utils/validation';
|
||||
@@ -26,7 +26,7 @@ export class CourseCloneApi extends BaseApiWithErrors {
|
||||
*/
|
||||
async clone(courseId: string, title: string, description?: string) {
|
||||
// Generate unique slug from the title
|
||||
const slug = generateSlug(title);
|
||||
const slug = generateSlug(title, { appendTimestamp: true });
|
||||
|
||||
// Prepare data for validation
|
||||
const data: TCourseClone = {
|
||||
|
||||
@@ -206,7 +206,7 @@
|
||||
const isExerciseUnlocked = $derived(exerciseContentItem?.isUnlocked ?? false);
|
||||
</script>
|
||||
|
||||
<Page.Header isSticky={true} class="min-h-[36px]">
|
||||
<Page.Header isSticky={true} class="z-250! min-h-[36px]">
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$questionnaire.title || 'Exercise'}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
|
||||
import type { AccountOrg } from '$features/app/types';
|
||||
import type { Component } from 'svelte';
|
||||
import BracesIcon from '@lucide/svelte/icons/braces';
|
||||
import TagIcon from '@lucide/svelte/icons/tag';
|
||||
import { isActive } from '$lib/utils/functions/app';
|
||||
|
||||
@@ -22,6 +23,7 @@ export interface NavItem {
|
||||
isExpanded?: boolean;
|
||||
items?: NavItem[]; // for nested items like settings
|
||||
isPaid?: boolean; // Show upgrade indicator for free plan users
|
||||
disabled?: boolean;
|
||||
// Metadata for breadcrumb generation
|
||||
useHashUrl?: boolean; // Use '#' as URL (for collapsible items like settings)
|
||||
nestedRoutes?: NestedRouteConfig[]; // Static nested routes (like community/ask, settings/customize-lms)
|
||||
@@ -33,6 +35,7 @@ export interface NavItemConfig {
|
||||
path: string;
|
||||
icon?: Component;
|
||||
requiresAdmin?: boolean;
|
||||
disableWhenNotAdmin?: boolean;
|
||||
items?: NavItemConfig[];
|
||||
useHashUrl?: boolean; // Use '#' as URL (for collapsible items like settings)
|
||||
nestedRoutes?: NestedRouteConfig[]; // Static nested routes
|
||||
@@ -99,6 +102,14 @@ const baseNavConfig: NavItemConfig[] = [
|
||||
requiresAdmin: true,
|
||||
matchPattern: '^/org/[^/]+/setup(/.*)?$' // Matches nested routes
|
||||
},
|
||||
{
|
||||
titleKey: 'org_navigation.automation',
|
||||
path: '/automation',
|
||||
icon: BracesIcon,
|
||||
requiresAdmin: true,
|
||||
disableWhenNotAdmin: true,
|
||||
matchPattern: '^/org/[^/]+/automation(/.*)?$'
|
||||
},
|
||||
{
|
||||
titleKey: 'org_navigation.settings',
|
||||
path: '/settings',
|
||||
@@ -172,7 +183,7 @@ export function getOrgNavigationItems(
|
||||
|
||||
for (const config of baseNavConfig) {
|
||||
// Skip admin-only items if user is not admin
|
||||
if (config.requiresAdmin && !isOrgAdmin) {
|
||||
if (config.requiresAdmin && !isOrgAdmin && !config.disableWhenNotAdmin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -190,6 +201,7 @@ export function getOrgNavigationItems(
|
||||
icon: config.icon,
|
||||
isActive: isActive(pagePathname, fullPath, matchPattern),
|
||||
isExpanded: config.items ? isActive(pagePathname, fullPath, matchPattern) : undefined,
|
||||
disabled: Boolean(config.disableWhenNotAdmin && !isOrgAdmin),
|
||||
useHashUrl: config.useHashUrl,
|
||||
nestedRoutes: config.nestedRoutes,
|
||||
supportsDynamicSegment: config.supportsDynamicSegment,
|
||||
|
||||
@@ -27,21 +27,37 @@
|
||||
{#snippet child({ props })}
|
||||
<HoverableItem>
|
||||
{#snippet children(isHovered)}
|
||||
<a href={item.url} {...props}>
|
||||
{#if item.icon}
|
||||
{@const Icon = item.icon}
|
||||
<Icon {isHovered} size={16} class="custom" />
|
||||
{#if item.disabled}
|
||||
<span
|
||||
{...props}
|
||||
aria-disabled="true"
|
||||
class="flex cursor-not-allowed items-center gap-2 opacity-50"
|
||||
>
|
||||
{#if item.icon}
|
||||
{@const Icon = item.icon}
|
||||
<Icon {isHovered} size={16} class="custom" />
|
||||
<span>{item.title}</span>
|
||||
{:else}
|
||||
<span>{item.title}</span>
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<a href={item.url} {...props}>
|
||||
{#if item.icon}
|
||||
{@const Icon = item.icon}
|
||||
<Icon {isHovered} size={16} class="custom" />
|
||||
|
||||
<span>{item.title}</span>
|
||||
{:else}
|
||||
<span>{item.title}</span>
|
||||
{/if}
|
||||
{#if item.items}
|
||||
<ChevronRightIcon
|
||||
class="custom ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
{/if}
|
||||
</a>
|
||||
<span>{item.title}</span>
|
||||
{:else}
|
||||
<span>{item.title}</span>
|
||||
{/if}
|
||||
{#if item.items}
|
||||
<ChevronRightIcon
|
||||
class="custom ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
{/if}
|
||||
</a>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</HoverableItem>
|
||||
{/snippet}
|
||||
|
||||
@@ -29,8 +29,7 @@ export const PUBLIC_ROUTES = [
|
||||
ROUTE.FORGOT,
|
||||
ROUTE.RESET,
|
||||
`^${ROUTE.PAGES}/.*`,
|
||||
// Course landing page only (excludes COURSE_ENROLL which requires auth)
|
||||
`^${ROUTE.COURSE}/[^/]+/?$`,
|
||||
`^/course/.*/enroll$`,
|
||||
`^${ROUTE.COURSES}/?$`,
|
||||
'/404',
|
||||
`^${ROUTE.VERIFY_EMAIL_ERROR}$`,
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
function generateSlug(title) {
|
||||
return title
|
||||
.match(/[^\W\s\d]*/g, '')
|
||||
.filter((v = '') => !!v.trim())
|
||||
.join('-')
|
||||
.toLowerCase()
|
||||
.concat(`-${new Date().getTime()}`);
|
||||
}
|
||||
|
||||
export default generateSlug;
|
||||
@@ -2474,32 +2474,17 @@
|
||||
"subtitle": "Forbind MCP-klienter nu, med Zapier og den offentlige API, der følger på det samme nøglesystem med organisationsomfang.",
|
||||
"coming_soon": "Kommer snart",
|
||||
"mcp": {
|
||||
"architecture": {
|
||||
"title": "Arkitektur",
|
||||
"description": "MCP-klienten og ClassroomIO API er separate tillidsgrænser. Den org-omfattede automatiseringsnøgle autentificerer klienten, og API'et forbliver kilden til sandhed for validering og vedholdenhed."
|
||||
},
|
||||
"flows": {
|
||||
"title": "Typiske flows",
|
||||
"description": "Agenten ejer ræsonnement og struktur. ClassroomIO accepterer kun normaliserede kurser, gemmer kladder og udgiver efter anmodning.",
|
||||
"prompt_title": "Spørg om udkast",
|
||||
"prompt_description": "Brugeren beder om et kursus, agenten strukturerer det, og 'create_course_draft' gemmer det i ClassroomIO før ethvert udgivelsestrin.",
|
||||
"pdf_title": "PDF til udkast",
|
||||
"pdf_description": "Agenten læser PDF'en eksternt, omstrukturerer den i sektioner og lektioner og sender derefter den normaliserede kladde til ClassroomIO.",
|
||||
"review_title": "Anmeldelse for at offentliggøre",
|
||||
"review_description": "Brugeren beder om ændringer, agenten opdaterer kladden, og publicering sker kun efter eksplicit godkendelse."
|
||||
},
|
||||
"setup": {
|
||||
"title": "Klient opsætning",
|
||||
"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 `@cio/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.",
|
||||
"cursor_description": "Indsæt dette i `~/.cursor/mcp.json` eller dit arbejdsområde MCP-konfiguration."
|
||||
},
|
||||
"keys": {
|
||||
"title": "MCP nøgler",
|
||||
"description": "Disse nøgler autentificerer den aktuelle organisation, når en MCP-klient ringer til ClassroomIO gennem `@cio/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",
|
||||
"scope_badge": "Udkast opret/læs/opdater/udgiv",
|
||||
"secret_once": "Denne hemmelighed vises én gang. Kopier det til din MCP-klient nu."
|
||||
}
|
||||
},
|
||||
@@ -2530,10 +2515,6 @@
|
||||
"permissions": {
|
||||
"disabled_title": "Skrivebeskyttet adgang",
|
||||
"disabled_description": "Kun organisationsadministratorer kan oprette, rotere eller tilbagekalde automatiseringsnøgler. Lærere kan gennemgå opsætningsinstruktionerne her."
|
||||
},
|
||||
"overview": {
|
||||
"title": "Automatiseringsadgang",
|
||||
"description": "Opret org-omfattede automatiseringsnøgler, tilslut understøttede klienter, og hold al maskinadgang bundet tilbage til denne organisation."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2474,32 +2474,17 @@
|
||||
"subtitle": "Verbinden Sie jetzt MCP-Clients, wobei Zapier und die öffentliche API auf demselben organisationsweiten Schlüsselsystem folgen.",
|
||||
"coming_soon": "Kommt bald",
|
||||
"mcp": {
|
||||
"architecture": {
|
||||
"title": "Architektur",
|
||||
"description": "Der MCP-Client und die ClassroomIO-API sind separate Vertrauensgrenzen. Der organisationsweite Automatisierungsschlüssel authentifiziert den Client und die API bleibt die Quelle der Wahrheit für Validierung und Persistenz."
|
||||
},
|
||||
"flows": {
|
||||
"title": "Typische Strömungen",
|
||||
"description": "Der Agent besitzt Argumentation und Struktur. ClassroomIO akzeptiert nur normalisierte Kursnutzlasten, speichert Entwürfe und veröffentlicht auf Anfrage.",
|
||||
"prompt_title": "Aufforderung zum Entwurf",
|
||||
"prompt_description": "Der Benutzer fragt nach einem Kurs, der Agent strukturiert ihn und „create_course_draft“ speichert ihn dann vor jedem Veröffentlichungsschritt in ClassroomIO.",
|
||||
"pdf_title": "PDF zum Entwurf",
|
||||
"pdf_description": "Der Agent liest das PDF extern, strukturiert es in Abschnitte und Lektionen um und sendet dann den normalisierten Entwurf an ClassroomIO.",
|
||||
"review_title": "Rezension zur Veröffentlichung",
|
||||
"review_description": "Der Benutzer bittet um Änderungen, der Agent aktualisiert den Entwurf und die Veröffentlichung erfolgt erst nach ausdrücklicher Genehmigung."
|
||||
},
|
||||
"setup": {
|
||||
"title": "Client-Setup",
|
||||
"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 „@cio/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.",
|
||||
"cursor_description": "Fügen Sie dies in „~/.cursor/mcp.json“ oder Ihre Workspace-MCP-Konfiguration ein."
|
||||
},
|
||||
"keys": {
|
||||
"title": "MCP-Schlüssel",
|
||||
"description": "Diese Schlüssel authentifizieren die aktuelle Organisation, wenn ein MCP-Client ClassroomIO über „@cio/mcp“ aufruft.",
|
||||
"description": "Diese Schlüssel authentifizieren die aktuelle Organisation, wenn ein MCP-Client ClassroomIO über „@classroomio/mcp“ aufruft.",
|
||||
"generate": "MCP-Schlüssel generieren",
|
||||
"scope_badge": "Entwurf erstellen/lesen/aktualisieren/veröffentlichen",
|
||||
"secret_once": "Dieses Geheimnis wird einmal gezeigt. Kopieren Sie es jetzt in Ihren MCP-Client."
|
||||
}
|
||||
},
|
||||
@@ -2530,10 +2515,6 @@
|
||||
"permissions": {
|
||||
"disabled_title": "Nur lesender Zugriff",
|
||||
"disabled_description": "Nur Organisationsadministratoren können Automatisierungsschlüssel erstellen, rotieren oder widerrufen. Lehrer können die Einrichtungsanweisungen hier einsehen."
|
||||
},
|
||||
"overview": {
|
||||
"title": "Automatisierungszugriff",
|
||||
"description": "Erstellen Sie organisationsweite Automatisierungsschlüssel, verbinden Sie unterstützte Clients und halten Sie den gesamten Maschinenzugriff an diese Organisation gebunden."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"seconds": "seconds",
|
||||
"snackbar_error": "error while sending code"
|
||||
},
|
||||
|
||||
"email_verification": {
|
||||
"errors": {
|
||||
"invalid_token": {
|
||||
@@ -52,21 +51,18 @@
|
||||
},
|
||||
"security_footer": "Your account security is our top priority. All verification attempts are logged for security monitoring."
|
||||
},
|
||||
|
||||
"delete_modal": {
|
||||
"label": "Delete",
|
||||
"content": "Are you sure?",
|
||||
"no": "No",
|
||||
"yes": "Yes"
|
||||
},
|
||||
|
||||
"new_course_modal": {
|
||||
"live_class_label": "Live Class",
|
||||
"live_class_subtitle": "Ideal for bootcamps where lessons are time based",
|
||||
"self_paced_label": "Self Paced",
|
||||
"self_paced_subtitle": "Best for courses where students can take lessons at their own pace"
|
||||
},
|
||||
|
||||
"onboarding": {
|
||||
"sell": "Sell courses online",
|
||||
"teach": "Teach my current students online",
|
||||
@@ -86,20 +82,17 @@
|
||||
"back": "Back",
|
||||
"continue": "Continue"
|
||||
},
|
||||
|
||||
"welcome_modal": {
|
||||
"we_at": "We at",
|
||||
"small_team": "are a small team with a large mission to empower educators with great software to help them make an impact in people's lives. Education is the most powerful gift you can give anyone, and it makes perfect sense to provide you with all the tools you need to give this GIFT to more people.",
|
||||
"thank_you": "Thank you for choosing us",
|
||||
"deeply_appreciate": "we deeply appreciate it and are at your service."
|
||||
},
|
||||
|
||||
"app": {
|
||||
"search": {
|
||||
"placeholder": "Search"
|
||||
}
|
||||
},
|
||||
|
||||
"login": {
|
||||
"welcome": "Welcome back",
|
||||
"email": "Your email",
|
||||
@@ -142,7 +135,6 @@
|
||||
"timer": "Activity timer",
|
||||
"tic_tac": "Tictac toe",
|
||||
"see_more": "See more",
|
||||
|
||||
"whats_new": "What's new",
|
||||
"launch_week": "Launch week"
|
||||
},
|
||||
@@ -365,7 +357,6 @@
|
||||
"video": "Video",
|
||||
"image": "Image"
|
||||
},
|
||||
|
||||
"show_banner": "Show banner",
|
||||
"hide_banner": "Hide banner"
|
||||
},
|
||||
@@ -422,7 +413,6 @@
|
||||
"twitter": "Twitter",
|
||||
"linkedin": "LinkedIn"
|
||||
},
|
||||
|
||||
"custom_links": {
|
||||
"heading": "Custom Links",
|
||||
"hide_links": "Hide Links",
|
||||
@@ -458,7 +448,6 @@
|
||||
"error": "An error occurred while opening billing"
|
||||
}
|
||||
},
|
||||
|
||||
"media_manager": {
|
||||
"page_title": "Media Manager - ClassroomIO",
|
||||
"heading": "Media Manager",
|
||||
@@ -567,7 +556,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"tags_admin": {
|
||||
"page_title": "Tags - ClassroomIO",
|
||||
"heading": "Tags",
|
||||
@@ -617,7 +605,6 @@
|
||||
"no_groups": "Create a tag group before creating tags"
|
||||
}
|
||||
},
|
||||
|
||||
"public_courses": {
|
||||
"page_title": "Courses - ClassroomIO",
|
||||
"heading": "Courses",
|
||||
@@ -633,7 +620,6 @@
|
||||
"description": "Try clearing your filters or check back later."
|
||||
}
|
||||
},
|
||||
|
||||
"org_navigation": {
|
||||
"platform": "Platform",
|
||||
"dashboard": "Dashboard",
|
||||
@@ -650,7 +636,6 @@
|
||||
"unlock": "Unlock unlimited features and invest in our future",
|
||||
"upgrade": "Upgrade Now"
|
||||
},
|
||||
|
||||
"pricing": {
|
||||
"plan_names": {
|
||||
"free": "Free",
|
||||
@@ -671,7 +656,6 @@
|
||||
"per_org": "Per Organization"
|
||||
}
|
||||
},
|
||||
|
||||
"setup": {
|
||||
"your_checklist": "Your Checklist",
|
||||
"get_started": "Get Started",
|
||||
@@ -711,7 +695,6 @@
|
||||
"button_label": "Publish Course"
|
||||
}
|
||||
},
|
||||
|
||||
"add_org": {
|
||||
"create_org": "Create Organization",
|
||||
"name": "Name of Organization",
|
||||
@@ -720,7 +703,6 @@
|
||||
"sitename": "Sitename already exists",
|
||||
"error_organization": "Something went wrong while creating this organization. Please reload and try again"
|
||||
},
|
||||
|
||||
"navigation": {
|
||||
"goto": "Go to",
|
||||
"courses": "Courses",
|
||||
@@ -739,12 +721,10 @@
|
||||
"org_loading_state": "Taking you to your organization...",
|
||||
"add_organization": "+ Add Organization"
|
||||
},
|
||||
|
||||
"markdown_editor": {
|
||||
"write": "Write",
|
||||
"preview": "Preview"
|
||||
},
|
||||
|
||||
"components": {
|
||||
"quiz": {
|
||||
"title": "Quizzes",
|
||||
@@ -825,7 +805,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"schedule": {
|
||||
"title": "Your Schedule",
|
||||
"essence": "The essence of design collaboration",
|
||||
@@ -833,7 +812,6 @@
|
||||
"join": "Join Call",
|
||||
"no": "No Lesson on this day"
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"morning_heading": "Good Morning",
|
||||
"afternoon_heading": "Good Afternoon",
|
||||
@@ -868,7 +846,6 @@
|
||||
"publish_first_course_description": "To track enrollments, publish a course and invite students",
|
||||
"publish_course": "Publish Course"
|
||||
},
|
||||
|
||||
"ai": {
|
||||
"help_me": "Help me write",
|
||||
"placeholder": "Help me write a short description",
|
||||
@@ -878,7 +855,6 @@
|
||||
"insert": "Insert",
|
||||
"can_you": "Can you please rephrase this"
|
||||
},
|
||||
|
||||
"courses": {
|
||||
"heading": "Courses",
|
||||
"heading_button": "Create Course",
|
||||
@@ -947,7 +923,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"course": {
|
||||
"navItems": {
|
||||
"nav_news_feed": "News Feed",
|
||||
@@ -972,7 +947,6 @@
|
||||
"placeholder": "Search lessons or exercises",
|
||||
"empty": "No results found"
|
||||
},
|
||||
|
||||
"navItem": {
|
||||
"news_feed": {
|
||||
"heading": "News Feed",
|
||||
@@ -1015,7 +989,6 @@
|
||||
"fetching_error": "An error occured while fetching feed"
|
||||
}
|
||||
},
|
||||
|
||||
"lessons": {
|
||||
"heading": "Lessons",
|
||||
"heading_v2": "Content",
|
||||
@@ -1078,7 +1051,6 @@
|
||||
"no_tutor": "No tutor added",
|
||||
"body_header": "No lessons yet",
|
||||
"body_content": "Share your knowledge with the world by creating engaging lessons. Start by clicking on the Add button.",
|
||||
|
||||
"lesson_nav": {
|
||||
"materials": "Materials",
|
||||
"exercises": "Exercises"
|
||||
@@ -1179,7 +1151,6 @@
|
||||
"upload_error_try_again": "Please try again with a valid document file",
|
||||
"try_again": "Try Again",
|
||||
"upgrade": "Upgrade your plan to upload documents",
|
||||
|
||||
"cancel_upload": "Cancel Upload",
|
||||
"upload_cancelled": "Upload cancelled by user",
|
||||
"downloading": "Downloading...",
|
||||
@@ -1197,7 +1168,6 @@
|
||||
},
|
||||
"button_done": "Done"
|
||||
},
|
||||
|
||||
"exercises": {
|
||||
"heading": "All Exercises",
|
||||
"add_button": "Add",
|
||||
@@ -1554,7 +1524,6 @@
|
||||
"load_more_btn": "Load more",
|
||||
"comment_error": "Failed to post comment. Please try again."
|
||||
},
|
||||
|
||||
"polls": {
|
||||
"title": "Poll",
|
||||
"active_polls": "Active Polls",
|
||||
@@ -1562,7 +1531,6 @@
|
||||
"loading": "Loading...",
|
||||
"no_polls": "No polls to display"
|
||||
},
|
||||
|
||||
"poll": {
|
||||
"voted": "Voted",
|
||||
"created_by": "Created by",
|
||||
@@ -1591,7 +1559,6 @@
|
||||
"restore_version": "Restore this version"
|
||||
}
|
||||
},
|
||||
|
||||
"attendance": {
|
||||
"title": "Attendance",
|
||||
"present": "Present",
|
||||
@@ -1601,7 +1568,6 @@
|
||||
"lesson": "Lesson",
|
||||
"no_student": "No Student Added"
|
||||
},
|
||||
|
||||
"submissions": {
|
||||
"title": "Submitted Exercises",
|
||||
"submission_status": {
|
||||
@@ -1640,7 +1606,6 @@
|
||||
"delete_success": "Successfully deleted submission"
|
||||
}
|
||||
},
|
||||
|
||||
"marks": {
|
||||
"title": "Marks",
|
||||
"student": "Student",
|
||||
@@ -1652,7 +1617,6 @@
|
||||
},
|
||||
"no_student": "No Student Added"
|
||||
},
|
||||
|
||||
"people": {
|
||||
"title": "People",
|
||||
"add": "Add",
|
||||
@@ -1761,7 +1725,6 @@
|
||||
"remove": "Remove"
|
||||
}
|
||||
},
|
||||
|
||||
"certificates": {
|
||||
"title": "Certificates",
|
||||
"certificate_settings": "Certificate Settings",
|
||||
@@ -1789,7 +1752,6 @@
|
||||
"characters": "characters",
|
||||
"unexpected_error": "An unexpected error occurred"
|
||||
},
|
||||
|
||||
"landing_page": {
|
||||
"editor": {
|
||||
"title": {
|
||||
@@ -1802,12 +1764,10 @@
|
||||
"pricing": "Pricing",
|
||||
"certificate": "Certificate"
|
||||
},
|
||||
|
||||
"save": "Save",
|
||||
"page_builder": "Page Builder",
|
||||
"section": "section",
|
||||
"display_section": "Display section",
|
||||
|
||||
"header_form": {
|
||||
"title": "Title",
|
||||
"description": "Description",
|
||||
@@ -1816,7 +1776,6 @@
|
||||
"replace": "Replace",
|
||||
"helper": "Enter a link to a youtube video"
|
||||
},
|
||||
|
||||
"reviews_form": {
|
||||
"add_reviews": "Add Reviews",
|
||||
"fullname": "Full Name",
|
||||
@@ -1838,11 +1797,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"certificate_form": {
|
||||
"upload_template": "Certificate template"
|
||||
},
|
||||
|
||||
"instructor_form": {
|
||||
"upload": "Instructor Logo",
|
||||
"name": "Instructor name",
|
||||
@@ -1852,7 +1809,6 @@
|
||||
"about_placeholder": "A short background about the instructure",
|
||||
"total": "Total number of courses"
|
||||
},
|
||||
|
||||
"pricing_form": {
|
||||
"currency": "Currency",
|
||||
"cost": "Cost",
|
||||
@@ -1864,7 +1820,6 @@
|
||||
"gift": "About Gift"
|
||||
}
|
||||
},
|
||||
|
||||
"upload_widget": {
|
||||
"title": "Banner Widget",
|
||||
"width": "Images wider than 1500 pixels work best.",
|
||||
@@ -1873,7 +1828,6 @@
|
||||
"submit": "Submit",
|
||||
"no_images": "No images available."
|
||||
},
|
||||
|
||||
"pricing_section": {
|
||||
"free": "Free",
|
||||
"not_accepting": "This course is currently not accepting new students.",
|
||||
@@ -1887,7 +1841,6 @@
|
||||
"back_to_course": "Back to course",
|
||||
"join_course": "Join Course"
|
||||
},
|
||||
|
||||
"course_content": "Course content",
|
||||
"modules": "modules",
|
||||
"lessons": "lessons",
|
||||
@@ -1923,7 +1876,6 @@
|
||||
"explore": "Explore courses",
|
||||
"find": "Find courses you will love from best teachers all over the world",
|
||||
"find_course": "Find a course...",
|
||||
|
||||
"reviews_modal": {
|
||||
"title": "Reviews",
|
||||
"rating": "Rating",
|
||||
@@ -1932,11 +1884,9 @@
|
||||
"instructor": "Instructor",
|
||||
"courses": "courses"
|
||||
},
|
||||
|
||||
"analytics": {
|
||||
"title": "Analytics"
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"heading": "Settings",
|
||||
"cover_image": "Cover Image",
|
||||
@@ -1987,14 +1937,12 @@
|
||||
"save": "Save Changes"
|
||||
}
|
||||
},
|
||||
|
||||
"not_permitted": {
|
||||
"header": "No Permission",
|
||||
"body": "Unfortunately you don't have access to this course.",
|
||||
"button": "Go home"
|
||||
}
|
||||
},
|
||||
|
||||
"community": {
|
||||
"title": "Community",
|
||||
"ask_button": "Ask Community",
|
||||
@@ -2027,7 +1975,6 @@
|
||||
},
|
||||
"all": "All"
|
||||
},
|
||||
|
||||
"audience": {
|
||||
"title": "Audience",
|
||||
"export": "Export",
|
||||
@@ -2038,7 +1985,6 @@
|
||||
"date_joined": "Date Joined",
|
||||
"upgrade": "Upgrade your plan to invite more students"
|
||||
},
|
||||
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
"courses": "Courses",
|
||||
@@ -2098,7 +2044,6 @@
|
||||
"no_data_default": "No data available",
|
||||
"no_data_default_description": "There is no data to display at this time."
|
||||
},
|
||||
|
||||
"upgrade": {
|
||||
"certificate": "Upgrade your plan to generate certificates",
|
||||
"team": "Upgrade your plan to add team members",
|
||||
@@ -2110,15 +2055,10 @@
|
||||
"required": "Upgrade Required",
|
||||
"enterprise_required": "This feature requires an Enterprise plan."
|
||||
},
|
||||
|
||||
"automation": {
|
||||
"title": "Automation",
|
||||
"subtitle": "Connect MCP clients now, with Zapier and the public API following on the same org-scoped key system.",
|
||||
"coming_soon": "Coming soon",
|
||||
"overview": {
|
||||
"title": "Automation access",
|
||||
"description": "Create org-scoped automation keys, connect supported clients, and keep all machine access tied back to this organization."
|
||||
},
|
||||
"permissions": {
|
||||
"disabled_title": "Read-only access",
|
||||
"disabled_description": "Only organization admins can create, rotate, or revoke automation keys. Teachers can review the setup instructions here."
|
||||
@@ -2150,35 +2090,19 @@
|
||||
"mcp": {
|
||||
"keys": {
|
||||
"title": "MCP keys",
|
||||
"description": "These keys authenticate the current organization when an MCP client calls ClassroomIO through `@cio/mcp`.",
|
||||
"description": "These keys authenticate the current organization when an MCP client calls ClassroomIO through `@classroomio/mcp`.",
|
||||
"generate": "Generate MCP key",
|
||||
"scope_badge": "Draft create/read/update/publish",
|
||||
"secret_once": "This secret is shown once. Copy it into your MCP client now."
|
||||
},
|
||||
"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.",
|
||||
"claude_code_description": "Registers `@cio/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.",
|
||||
"cursor_description": "Paste this into `~/.cursor/mcp.json` or your workspace MCP config."
|
||||
},
|
||||
"flows": {
|
||||
"title": "Typical flows",
|
||||
"description": "The agent owns reasoning and structure. ClassroomIO only accepts normalized course payloads, saves drafts, and publishes on request.",
|
||||
"prompt_title": "Prompt to draft",
|
||||
"prompt_description": "The user asks for a course, the agent structures it, then `create_course_draft` saves it into ClassroomIO before any publish step.",
|
||||
"pdf_title": "PDF to draft",
|
||||
"pdf_description": "The agent reads the PDF externally, restructures it into sections and lessons, then sends the normalized draft into ClassroomIO.",
|
||||
"review_title": "Review to publish",
|
||||
"review_description": "The user asks for changes, the agent updates the draft, and publish only happens after explicit approval."
|
||||
},
|
||||
"architecture": {
|
||||
"title": "Architecture",
|
||||
"description": "The MCP client and ClassroomIO API are separate trust boundaries. The org-scoped automation key authenticates the client, and the API remains the source of truth for validation and persistence."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"lms_navigation": {
|
||||
"home": "Home",
|
||||
"my_learning": "My Learning",
|
||||
@@ -2188,7 +2112,6 @@
|
||||
"help": "Help",
|
||||
"settings": "Settings"
|
||||
},
|
||||
|
||||
"my_learning": {
|
||||
"heading": "My Learning",
|
||||
"progress": "In progress",
|
||||
@@ -2199,7 +2122,6 @@
|
||||
"not_completed": "No Course Completed",
|
||||
"any_course": "Any course that you complete will be displayed here"
|
||||
},
|
||||
|
||||
"exercises": {
|
||||
"heading": "Exercises",
|
||||
"not_submitted": "Not Submitted",
|
||||
@@ -2208,13 +2130,11 @@
|
||||
"graded": "Graded",
|
||||
"lesson": "Lesson"
|
||||
},
|
||||
|
||||
"explore": {
|
||||
"heading": "Explore our courses",
|
||||
"empty_heading": "No course available",
|
||||
"empty_description": "There are currently no published course available"
|
||||
},
|
||||
|
||||
"snackbar": {
|
||||
"success": "Success",
|
||||
"success_update": "Updated successfully",
|
||||
@@ -2251,7 +2171,6 @@
|
||||
"thumbnail_too_large": "Thumbnail image is too large",
|
||||
"thumbnail_invalid": "Invalid thumbnail file"
|
||||
},
|
||||
|
||||
"poll": {
|
||||
"error": {
|
||||
"creating_poll": "An error occurred while creating poll",
|
||||
@@ -2263,19 +2182,16 @@
|
||||
"vote_once": "You can only vote once"
|
||||
}
|
||||
},
|
||||
|
||||
"exercise": {
|
||||
"success": "Saved Successfully",
|
||||
"submission_updated": "Submission updated to",
|
||||
"error_fetching": "Fetching exercises failed, please reload the page"
|
||||
},
|
||||
|
||||
"materials": {
|
||||
"apology": "We apologise, there was an error saving your lesson.",
|
||||
"update_translations": "Updating translations failed",
|
||||
"creating_new": "Creating new translations failed"
|
||||
},
|
||||
|
||||
"newsfeed": {
|
||||
"success": {
|
||||
"edit": "Feed edited successfully",
|
||||
@@ -2287,7 +2203,6 @@
|
||||
"creating": "creating feed"
|
||||
}
|
||||
},
|
||||
|
||||
"people": {
|
||||
"error": {
|
||||
"missing_data": "Org data missing, please reload the page",
|
||||
@@ -2297,7 +2212,6 @@
|
||||
"copied": "Copied to clipboard"
|
||||
}
|
||||
},
|
||||
|
||||
"course_settings": {
|
||||
"success": {
|
||||
"download": "Download Complete",
|
||||
@@ -2315,7 +2229,6 @@
|
||||
"failed_deletion": "Deletion Failed, please try again later"
|
||||
}
|
||||
},
|
||||
|
||||
"lms": {
|
||||
"error": {
|
||||
"username_exists": "username already exists",
|
||||
@@ -2323,7 +2236,6 @@
|
||||
"try_again": "Please try again"
|
||||
}
|
||||
},
|
||||
|
||||
"team_members": {
|
||||
"invite_sent": "Invite Sent!",
|
||||
"user_exists": "User Already Exists",
|
||||
@@ -2331,7 +2243,6 @@
|
||||
"remove_fail": "Failed to remove user, please try again",
|
||||
"remove_success": "Member removed successfully"
|
||||
},
|
||||
|
||||
"tags": {
|
||||
"group_created": "Tag group created",
|
||||
"group_updated": "Tag group updated",
|
||||
@@ -2341,7 +2252,6 @@
|
||||
"fetch_failed": "Failed to load tags",
|
||||
"save_failed": "Failed to save tag changes"
|
||||
},
|
||||
|
||||
"landing_page_settings": {
|
||||
"error": {
|
||||
"file_size": "File Size must not be greater than 500kb",
|
||||
@@ -2353,7 +2263,6 @@
|
||||
"complete": "Complete :)"
|
||||
}
|
||||
},
|
||||
|
||||
"course": {
|
||||
"success": {
|
||||
"comment_deleted": "Comment Deleted",
|
||||
@@ -2371,13 +2280,11 @@
|
||||
"feed_fetch_error": "An error occured while fetching feed"
|
||||
}
|
||||
},
|
||||
|
||||
"attendance": {
|
||||
"error": {
|
||||
"reload": "Something isn't right. Please reload and take attendance again"
|
||||
}
|
||||
},
|
||||
|
||||
"lessons": {
|
||||
"lesson_created": "Lesson created successfully",
|
||||
"lesson_create_failed": "Failed to create lesson",
|
||||
@@ -2410,28 +2317,23 @@
|
||||
"exercise_not_complete": "Complete all exercises to mark as complete"
|
||||
}
|
||||
},
|
||||
|
||||
"marks": {
|
||||
"error": "Error fetching marks"
|
||||
},
|
||||
|
||||
"submissions": {
|
||||
"success": {
|
||||
"grading": "Grading saved and student notified"
|
||||
}
|
||||
},
|
||||
|
||||
"invite": {
|
||||
"failed_join": "Joining failed, please contact your admin"
|
||||
},
|
||||
|
||||
"community": {
|
||||
"error": {
|
||||
"try_again": "Error - Please try again later",
|
||||
"deleting_comments": "Error deleting comments",
|
||||
"deleting_question": "Error deleting question"
|
||||
},
|
||||
|
||||
"success": {
|
||||
"question_updated": "Question Updated!",
|
||||
"comment_submitted": "Comment Submitted!",
|
||||
@@ -2439,31 +2341,25 @@
|
||||
"question_submitted": "Question Submitted!"
|
||||
}
|
||||
},
|
||||
|
||||
"signup": {
|
||||
"disposable": "The email you provided is disposable"
|
||||
},
|
||||
|
||||
"upgrade": {
|
||||
"generate_fail": "Failed to generate success",
|
||||
"failed": "Failed, please try again later"
|
||||
},
|
||||
|
||||
"setup": {
|
||||
"info_lesson": "You Need to Create a lesson",
|
||||
"info_course": "You need to create a course"
|
||||
},
|
||||
|
||||
"generic": {
|
||||
"success_delete": "Deleted successfully",
|
||||
"try_again": "Error - Please try again later"
|
||||
}
|
||||
},
|
||||
|
||||
"specialization": {
|
||||
"course_tag": "Specialization"
|
||||
},
|
||||
|
||||
"invite": {
|
||||
"organization": {
|
||||
"page_title": "Join {orgName} on ClassroomIO",
|
||||
@@ -2481,18 +2377,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"content": {
|
||||
"heading": "Internationalization in SvelteKit",
|
||||
"toggle_label": "Select Preferred Language"
|
||||
},
|
||||
|
||||
"generic": {
|
||||
"locked": "Locked",
|
||||
"unlocked": "Unlocked",
|
||||
"loading": "Loading"
|
||||
},
|
||||
|
||||
"validations": {
|
||||
"user_profile": {
|
||||
"email": "Invalid email address",
|
||||
@@ -2619,7 +2512,6 @@
|
||||
"invalid_field": "Invalid value"
|
||||
}
|
||||
},
|
||||
|
||||
"common": {
|
||||
"unsaved_changes": {
|
||||
"message": "You have unsaved changes. Are you sure you want to leave?"
|
||||
|
||||
@@ -2474,32 +2474,17 @@
|
||||
"subtitle": "Conecte los clientes de MCP ahora, con Zapier y la API pública siguiendo el mismo sistema de claves con ámbito de organización.",
|
||||
"coming_soon": "Próximamente",
|
||||
"mcp": {
|
||||
"architecture": {
|
||||
"title": "Arquitectura",
|
||||
"description": "El cliente MCP y la API ClassroomIO son límites de confianza separados. La clave de automatización con ámbito de organización autentica al cliente y la API sigue siendo la fuente de verdad para la validación y la persistencia."
|
||||
},
|
||||
"flows": {
|
||||
"title": "Flujos típicos",
|
||||
"description": "El agente posee razonamiento y estructura. ClassroomIO solo acepta cargas útiles de cursos normalizados, guarda borradores y publica a pedido.",
|
||||
"prompt_title": "Solicitar borrador",
|
||||
"prompt_description": "El usuario solicita un curso, el agente lo estructura y luego `create_course_draft` lo guarda en ClassroomIO antes de cualquier paso de publicación.",
|
||||
"pdf_title": "PDF para borrador",
|
||||
"pdf_description": "El agente lee el PDF externamente, lo reestructura en secciones y lecciones y luego envía el borrador normalizado a ClassroomIO.",
|
||||
"review_title": "Revisar para publicar",
|
||||
"review_description": "El usuario solicita cambios, el agente actualiza el borrador y la publicación solo se realiza después de una aprobación explícita."
|
||||
},
|
||||
"setup": {
|
||||
"title": "Configuración del cliente",
|
||||
"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 `@cio/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.",
|
||||
"cursor_description": "Pegue esto en `~/.cursor/mcp.json` o en la configuración MCP de su espacio de trabajo."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Claves MCP",
|
||||
"description": "Estas claves autentican la organización actual cuando un cliente MCP llama a ClassroomIO a través de `@cio/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",
|
||||
"scope_badge": "Borrador de creación/lectura/actualización/publicación",
|
||||
"secret_once": "Este secreto se muestra una vez. Cópielo en su cliente MCP ahora."
|
||||
}
|
||||
},
|
||||
@@ -2530,10 +2515,6 @@
|
||||
"permissions": {
|
||||
"disabled_title": "Acceso de solo lectura",
|
||||
"disabled_description": "Solo los administradores de la organización pueden crear, rotar o revocar claves de automatización. Los profesores pueden revisar las instrucciones de configuración aquí."
|
||||
},
|
||||
"overview": {
|
||||
"title": "Acceso a la automatización",
|
||||
"description": "Cree claves de automatización específicas de la organización, conecte clientes compatibles y mantenga todo el acceso a las máquinas vinculado a esta organización."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2474,32 +2474,17 @@
|
||||
"subtitle": "Connectez les clients MCP maintenant, avec Zapier et l'API publique qui suivent sur le même système de clés à l'échelle de l'organisation.",
|
||||
"coming_soon": "Bientôt disponible",
|
||||
"mcp": {
|
||||
"architecture": {
|
||||
"title": "Architecture",
|
||||
"description": "Le client MCP et l'API ClassroomIO sont des limites de confiance distinctes. La clé d'automatisation à l'échelle de l'organisation authentifie le client et l'API reste la source de vérité pour la validation et la persistance."
|
||||
},
|
||||
"flows": {
|
||||
"title": "Flux typiques",
|
||||
"description": "L'agent possède le raisonnement et la structure. ClassroomIO n'accepte que les charges utiles de cours normalisées, enregistre les brouillons et publie sur demande.",
|
||||
"prompt_title": "Invite à rédiger",
|
||||
"prompt_description": "L'utilisateur demande un cours, l'agent le structure, puis `create_course_draft` l'enregistre dans ClassroomIO avant toute étape de publication.",
|
||||
"pdf_title": "PDF à rédiger",
|
||||
"pdf_description": "L'agent lit le PDF en externe, le restructure en sections et leçons, puis envoie le brouillon normalisé dans ClassroomIO.",
|
||||
"review_title": "Revue à publier",
|
||||
"review_description": "L'utilisateur demande des modifications, l'agent met à jour le brouillon et la publication n'a lieu qu'après approbation explicite."
|
||||
},
|
||||
"setup": {
|
||||
"title": "Configuration du client",
|
||||
"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 `@cio/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.",
|
||||
"cursor_description": "Collez-le dans `~/.cursor/mcp.json` ou dans la configuration MCP de votre espace de travail."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Clés MCP",
|
||||
"description": "Ces clés authentifient l'organisation actuelle lorsqu'un client MCP appelle ClassroomIO via `@cio/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",
|
||||
"scope_badge": "Brouillon créer/lire/mettre à jour/publier",
|
||||
"secret_once": "Ce secret est montré une fois. Copiez-le dans votre client MCP maintenant."
|
||||
}
|
||||
},
|
||||
@@ -2530,10 +2515,6 @@
|
||||
"permissions": {
|
||||
"disabled_title": "Accès en lecture seule",
|
||||
"disabled_description": "Seuls les administrateurs de l'organisation peuvent créer, alterner ou révoquer des clés d'automatisation. Les enseignants peuvent consulter les instructions de configuration ici."
|
||||
},
|
||||
"overview": {
|
||||
"title": "Accès à l'automatisation",
|
||||
"description": "Créez des clés d'automatisation à l'échelle de l'organisation, connectez les clients pris en charge et conservez tous les accès aux machines liés à cette organisation."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2474,32 +2474,17 @@
|
||||
"subtitle": "एक ही ऑर्ग-स्कोप्ड कुंजी सिस्टम पर जैपियर और सार्वजनिक एपीआई के साथ अब एमसीपी क्लाइंट को कनेक्ट करें।",
|
||||
"coming_soon": "जल्द आ रहा है",
|
||||
"mcp": {
|
||||
"architecture": {
|
||||
"title": "वास्तुकला",
|
||||
"description": "एमसीपी क्लाइंट और क्लासरूमआईओ एपीआई अलग-अलग ट्रस्ट सीमाएँ हैं। ऑर्ग-स्कोप्ड ऑटोमेशन कुंजी क्लाइंट को प्रमाणित करती है, और एपीआई सत्यापन और दृढ़ता के लिए सत्य का स्रोत बना रहता है।"
|
||||
},
|
||||
"flows": {
|
||||
"title": "विशिष्ट प्रवाह",
|
||||
"description": "एजेंट के पास तर्क और संरचना होती है। क्लासरूमआईओ केवल सामान्यीकृत पाठ्यक्रम पेलोड स्वीकार करता है, ड्राफ्ट सहेजता है, और अनुरोध पर प्रकाशित करता है।",
|
||||
"prompt_title": "मसौदा तैयार करने के लिए संकेत दें",
|
||||
"prompt_description": "उपयोगकर्ता एक पाठ्यक्रम के लिए पूछता है, एजेंट इसकी संरचना करता है, फिर `create_course_draft` किसी भी प्रकाशन चरण से पहले इसे ClassroomIO में सहेजता है।",
|
||||
"pdf_title": "ड्राफ्ट करने के लिए पीडीएफ",
|
||||
"pdf_description": "एजेंट पीडीएफ को बाहरी रूप से पढ़ता है, इसे अनुभागों और पाठों में पुनर्गठित करता है, फिर सामान्यीकृत ड्राफ्ट को क्लासरूमआईओ में भेजता है।",
|
||||
"review_title": "प्रकाशित करने हेतु समीक्षा करें",
|
||||
"review_description": "उपयोगकर्ता परिवर्तन मांगता है, एजेंट ड्राफ्ट को अपडेट करता है, और प्रकाशन केवल स्पष्ट अनुमोदन के बाद ही होता है।"
|
||||
},
|
||||
"setup": {
|
||||
"title": "क्लाइंट सेटअप",
|
||||
"description": "प्रति एकीकरण एक ऑर्ग-स्कोप्ड कुंजी का उपयोग करें। यदि कोई कुंजी घुमाई जाती है या निरस्त की जाती है, तो क्लाइंट नया रहस्य कॉन्फ़िगर होने तक काम करना बंद कर देता है।",
|
||||
"claude_code_description": "उत्पन्न ऑर्ग-स्कोप्ड कुंजी का उपयोग करके क्लाउड कोड के साथ `@cio/mcp` को पंजीकृत करता है।",
|
||||
"claude_code_description": "उत्पन्न ऑर्ग-स्कोप्ड कुंजी का उपयोग करके क्लाउड कोड के साथ `@classroomio/mcp` को पंजीकृत करता है।",
|
||||
"codex_description": "समान ऑर्ग-स्कोप्ड कुंजी के साथ क्लासरूमआईओ एमसीपी सर्वर को कोडेक्स में जोड़ता है।",
|
||||
"cursor_description": "इसे `~/.cursor/mcp.json` या अपने कार्यक्षेत्र MCP कॉन्फ़िगरेशन में पेस्ट करें।"
|
||||
},
|
||||
"keys": {
|
||||
"title": "एमसीपी कुंजी",
|
||||
"description": "जब कोई MCP क्लाइंट `@cio/mcp` के माध्यम से ClassroomIO को कॉल करता है तो ये कुंजियाँ वर्तमान संगठन को प्रमाणित करती हैं।",
|
||||
"description": "जब कोई MCP क्लाइंट `@classroomio/mcp` के माध्यम से ClassroomIO को कॉल करता है तो ये कुंजियाँ वर्तमान संगठन को प्रमाणित करती हैं।",
|
||||
"generate": "MCP कुंजी जनरेट करें",
|
||||
"scope_badge": "ड्राफ्ट बनाएं/पढ़ें/अद्यतन करें/प्रकाशित करें",
|
||||
"secret_once": "यह रहस्य एक बार दिखाया गया है। अब इसे अपने MCP क्लाइंट में कॉपी करें।"
|
||||
}
|
||||
},
|
||||
@@ -2530,10 +2515,6 @@
|
||||
"permissions": {
|
||||
"disabled_title": "केवल पढ़ने योग्य पहुंच",
|
||||
"disabled_description": "केवल संगठन व्यवस्थापक ही स्वचालन कुंजियाँ बना सकते हैं, घुमा सकते हैं या निरस्त कर सकते हैं। शिक्षक यहां सेटअप निर्देशों की समीक्षा कर सकते हैं।"
|
||||
},
|
||||
"overview": {
|
||||
"title": "स्वचालन पहुंच",
|
||||
"description": "ऑर्ग-स्कोप्ड ऑटोमेशन कुंजियाँ बनाएँ, समर्थित क्लाइंट्स को कनेक्ट करें, और सभी मशीन एक्सेस को इस संगठन से जोड़े रखें।"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2474,32 +2474,17 @@
|
||||
"subtitle": "Połącz teraz klientów MCP, korzystając z Zapiera i publicznego interfejsu API w tym samym systemie kluczy o zasięgu organizacji.",
|
||||
"coming_soon": "Już wkrótce",
|
||||
"mcp": {
|
||||
"architecture": {
|
||||
"title": "Architektura",
|
||||
"description": "Klient MCP i interfejs API ClassroomIO stanowią oddzielne granice zaufania. Klucz automatyzacji o zasięgu organizacji uwierzytelnia klienta, a interfejs API pozostaje źródłem prawdy w celu sprawdzenia poprawności i trwałości."
|
||||
},
|
||||
"flows": {
|
||||
"title": "Typowe przepływy",
|
||||
"description": "Agent jest właścicielem rozumowania i struktury. ClassroomIO akceptuje tylko znormalizowane ładunki kursów, zapisuje wersje robocze i publikuje na żądanie.",
|
||||
"prompt_title": "Monituj o wersję roboczą",
|
||||
"prompt_description": "Użytkownik prosi o kurs, agent tworzy go, a następnie „create_course_draft” zapisuje go w ClassroomIO przed jakimkolwiek etapem publikacji.",
|
||||
"pdf_title": "PDF do wersji roboczej",
|
||||
"pdf_description": "Agent czyta plik PDF zewnętrznie, dzieli go na sekcje i lekcje, a następnie wysyła znormalizowaną wersję roboczą do ClassroomIO.",
|
||||
"review_title": "Recenzja do opublikowania",
|
||||
"review_description": "Użytkownik prosi o zmiany, agent aktualizuje wersję roboczą, a publikacja następuje dopiero po wyraźnej zgodzie."
|
||||
},
|
||||
"setup": {
|
||||
"title": "Konfiguracja klienta",
|
||||
"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 `@cio/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.",
|
||||
"cursor_description": "Wklej to do `~/.cursor/mcp.json` lub konfiguracji MCP w obszarze roboczym."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Klucze MCP",
|
||||
"description": "Te klucze uwierzytelniają bieżącą organizację, gdy klient MCP wywołuje ClassroomIO poprzez `@cio/mcp`.",
|
||||
"description": "Te klucze uwierzytelniają bieżącą organizację, gdy klient MCP wywołuje ClassroomIO poprzez `@classroomio/mcp`.",
|
||||
"generate": "Wygeneruj klucz MCP",
|
||||
"scope_badge": "Utwórz/odczytaj/aktualizuj/publikuj wersję roboczą",
|
||||
"secret_once": "Sekret ten zostaje pokazany raz. Skopiuj go teraz do swojego klienta MCP."
|
||||
}
|
||||
},
|
||||
@@ -2530,10 +2515,6 @@
|
||||
"permissions": {
|
||||
"disabled_title": "Dostęp tylko do odczytu",
|
||||
"disabled_description": "Tylko administratorzy organizacji mogą tworzyć, obracać i unieważniać klucze automatyzacji. Nauczyciele mogą zapoznać się z instrukcjami konfiguracji tutaj."
|
||||
},
|
||||
"overview": {
|
||||
"title": "Dostęp do automatyki",
|
||||
"description": "Twórz klucze automatyzacji na poziomie organizacji, łącz obsługiwanych klientów i powiązuj cały dostęp do maszyn z tą organizacją."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2474,32 +2474,17 @@
|
||||
"subtitle": "Conecte clientes MCP agora, com o Zapier e a API pública seguindo o mesmo sistema de chaves com escopo organizacional.",
|
||||
"coming_soon": "Em breve",
|
||||
"mcp": {
|
||||
"architecture": {
|
||||
"title": "Arquitetura",
|
||||
"description": "O cliente MCP e a API ClassroomIO são limites de confiança separados. A chave de automação no escopo organizacional autentica o cliente, e a API continua sendo a fonte da verdade para validação e persistência."
|
||||
},
|
||||
"flows": {
|
||||
"title": "Fluxos típicos",
|
||||
"description": "O agente possui raciocínio e estrutura. ClassroomIO aceita apenas cargas de curso normalizadas, salva rascunhos e publica mediante solicitação.",
|
||||
"prompt_title": "Solicitar rascunho",
|
||||
"prompt_description": "O usuário solicita um curso, o agente o estrutura e, em seguida, `create_course_draft` o salva no ClassroomIO antes de qualquer etapa de publicação.",
|
||||
"pdf_title": "PDF para rascunho",
|
||||
"pdf_description": "O agente lê o PDF externamente, reestrutura-o em seções e lições e, em seguida, envia o rascunho normalizado para o ClassroomIO.",
|
||||
"review_title": "Revise para publicar",
|
||||
"review_description": "O usuário solicita alterações, o agente atualiza o rascunho e a publicação só acontece após aprovação explícita."
|
||||
},
|
||||
"setup": {
|
||||
"title": "Configuração do cliente",
|
||||
"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 `@cio/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.",
|
||||
"cursor_description": "Cole isso em `~/.cursor/mcp.json` ou na configuração do MCP do seu espaço de trabalho."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Chaves MCP",
|
||||
"description": "Essas chaves autenticam a organização atual quando um cliente MCP chama ClassroomIO por meio de `@cio/mcp`.",
|
||||
"description": "Essas chaves autenticam a organização atual quando um cliente MCP chama ClassroomIO por meio de `@classroomio/mcp`.",
|
||||
"generate": "Gerar chave MCP",
|
||||
"scope_badge": "Rascunho criar/ler/atualizar/publicar",
|
||||
"secret_once": "Este segredo é mostrado uma vez. Copie-o para o seu cliente MCP agora."
|
||||
}
|
||||
},
|
||||
@@ -2530,10 +2515,6 @@
|
||||
"permissions": {
|
||||
"disabled_title": "Acesso somente leitura",
|
||||
"disabled_description": "Somente administradores da organização podem criar, alternar ou revogar chaves de automação. Os professores podem revisar as instruções de configuração aqui."
|
||||
},
|
||||
"overview": {
|
||||
"title": "Acesso de automação",
|
||||
"description": "Crie chaves de automação no escopo da organização, conecte clientes compatíveis e mantenha todo o acesso às máquinas vinculado a esta organização."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2474,32 +2474,17 @@
|
||||
"subtitle": "Подключите клиентов MCP прямо сейчас, используя Zapier и общедоступный API в одной и той же системе ключей на уровне организации.",
|
||||
"coming_soon": "Скоро",
|
||||
"mcp": {
|
||||
"architecture": {
|
||||
"title": "Архитектура",
|
||||
"description": "Клиент MCP и API ClassroomIO представляют собой отдельные границы доверия. Ключ автоматизации на уровне организации аутентифицирует клиента, а API остается источником достоверных данных для проверки и сохранения."
|
||||
},
|
||||
"flows": {
|
||||
"title": "Типичные потоки",
|
||||
"description": "Агент владеет рассуждениями и структурой. ClassroomIO принимает только нормализованные полезные нагрузки курса, сохраняет черновики и публикует по запросу.",
|
||||
"prompt_title": "Подскажите черновик",
|
||||
"prompt_description": "Пользователь запрашивает курс, агент структурирует его, затем `create_course_draft` сохраняет его в ClassroomIO перед любым шагом публикации.",
|
||||
"pdf_title": "PDF для черновика",
|
||||
"pdf_description": "Агент читает PDF-файл извне, реструктурирует его на разделы и уроки, а затем отправляет нормализованный черновик в ClassroomIO.",
|
||||
"review_title": "Отзыв для публикации",
|
||||
"review_description": "Пользователь запрашивает изменения, агент обновляет черновик, а публикация происходит только после явного одобрения."
|
||||
},
|
||||
"setup": {
|
||||
"title": "Настройка клиента",
|
||||
"description": "Используйте один ключ на уровне организации для каждой интеграции. Если ключ заменен или отозван, клиент перестает работать до тех пор, пока не будет настроен новый секрет.",
|
||||
"claude_code_description": "Регистрирует `@cio/mcp` в Claude Code, используя сгенерированный ключ на уровне организации.",
|
||||
"claude_code_description": "Регистрирует `@classroomio/mcp` в Claude Code, используя сгенерированный ключ на уровне организации.",
|
||||
"codex_description": "Добавляет сервер ClassroomIO MCP в Кодекс с тем же ключом на уровне организации.",
|
||||
"cursor_description": "Вставьте это в `~/.cursor/mcp.json` или в конфигурацию MCP вашей рабочей области."
|
||||
},
|
||||
"keys": {
|
||||
"title": "Ключи MCP",
|
||||
"description": "Эти ключи аутентифицируют текущую организацию, когда клиент MCP вызывает ClassroomIO через @cio/mcp.",
|
||||
"description": "Эти ключи аутентифицируют текущую организацию, когда клиент MCP вызывает ClassroomIO через @classroomio/mcp.",
|
||||
"generate": "Создать ключ MCP",
|
||||
"scope_badge": "Черновик создать/прочитать/обновить/опубликовать",
|
||||
"secret_once": "Этот секрет раскрывается один раз. Скопируйте его в свой клиент MCP сейчас."
|
||||
}
|
||||
},
|
||||
@@ -2530,10 +2515,6 @@
|
||||
"permissions": {
|
||||
"disabled_title": "Доступ только для чтения",
|
||||
"disabled_description": "Только администраторы организации могут создавать, менять или отзывать ключи автоматизации. Учителя могут ознакомиться с инструкциями по настройке здесь."
|
||||
},
|
||||
"overview": {
|
||||
"title": "Автоматизация доступа",
|
||||
"description": "Создавайте ключи автоматизации на уровне организации, подключайте поддерживаемых клиентов и сохраняйте привязку всего доступа к машинам к этой организации."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2474,32 +2474,17 @@
|
||||
"subtitle": "Kết nối các máy khách MCP ngay bây giờ, với Zapier và API công khai trên cùng một hệ thống khóa trong phạm vi tổ chức.",
|
||||
"coming_soon": "Sắp có",
|
||||
"mcp": {
|
||||
"architecture": {
|
||||
"title": "Kiến trúc",
|
||||
"description": "Ứng dụng khách MCP và API ClassroomIO là các ranh giới tin cậy riêng biệt. Khóa tự động hóa trong phạm vi tổ chức sẽ xác thực ứng dụng khách và API vẫn là nguồn thông tin chính xác để xác thực và duy trì."
|
||||
},
|
||||
"flows": {
|
||||
"title": "Dòng chảy điển hình",
|
||||
"description": "Tác nhân sở hữu lý luận và cấu trúc. ClassroomIO chỉ chấp nhận tải trọng khóa học được chuẩn hóa, lưu bản nháp và xuất bản theo yêu cầu.",
|
||||
"prompt_title": "Nhắc soạn thảo",
|
||||
"prompt_description": "Người dùng yêu cầu một khóa học, nhân viên sẽ cấu trúc khóa học đó, sau đó `create_course_draft` lưu nó vào ClassroomIO trước bất kỳ bước xuất bản nào.",
|
||||
"pdf_title": "PDF thành bản nháp",
|
||||
"pdf_description": "Nhân viên đọc bản PDF bên ngoài, cấu trúc lại thành các phần và bài học, sau đó gửi bản nháp đã chuẩn hóa vào ClassroomIO.",
|
||||
"review_title": "Xem xét để xuất bản",
|
||||
"review_description": "Người dùng yêu cầu thay đổi, người đại diện cập nhật bản nháp và việc xuất bản chỉ diễn ra sau khi được phê duyệt rõ ràng."
|
||||
},
|
||||
"setup": {
|
||||
"title": "Thiết lập máy khách",
|
||||
"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ý `@cio/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.",
|
||||
"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."
|
||||
},
|
||||
"keys": {
|
||||
"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 `@cio/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",
|
||||
"scope_badge": "Bản nháp tạo/đọc/cập nhật/xuất bản",
|
||||
"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ờ."
|
||||
}
|
||||
},
|
||||
@@ -2530,10 +2515,6 @@
|
||||
"permissions": {
|
||||
"disabled_title": "Quyền truy cập chỉ đọc",
|
||||
"disabled_description": "Chỉ quản trị viên tổ chức mới có thể tạo, xoay hoặc thu hồi khóa tự động hóa. Thầy cô có thể xem lại hướng dẫn thiết lập tại đây."
|
||||
},
|
||||
"overview": {
|
||||
"title": "Truy cập tự động hóa",
|
||||
"description": "Tạo khóa tự động hóa trong phạm vi tổ chức, kết nối các máy khách được hỗ trợ và giữ tất cả quyền truy cập của máy được gắn chặt với tổ chức này."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import ShieldIcon from '@lucide/svelte/icons/shield';
|
||||
|
||||
const emailFromUrl = page.url.searchParams.get('email') ?? '';
|
||||
const isEmailPrefilled = !!emailFromUrl;
|
||||
|
||||
let fields = $state(Object.assign({}, LOGIN_FIELDS, emailFromUrl ? { email: emailFromUrl } : {}));
|
||||
|
||||
@@ -192,7 +193,8 @@
|
||||
bind:value={fields.email}
|
||||
oninput={handleEmailChange}
|
||||
placeholder="[email protected]"
|
||||
disabled={loading}
|
||||
disabled={loading || isEmailPrefilled}
|
||||
readonly={isEmailPrefilled}
|
||||
autofocus
|
||||
aria-invalid={errors.email ? 'true' : undefined}
|
||||
autocomplete="username"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
let { data } = $props();
|
||||
const emailFromUrl = page.url.searchParams.get('email') ?? '';
|
||||
const isEmailPrefilled = !!emailFromUrl;
|
||||
let fields = $state(Object.assign({}, SIGNUP_FIELDS, emailFromUrl ? { email: emailFromUrl } : {}));
|
||||
let loading = $state(false);
|
||||
let errors: {
|
||||
@@ -160,7 +161,8 @@
|
||||
type="email"
|
||||
bind:value={fields.email}
|
||||
placeholder="[email protected]"
|
||||
disabled={loading}
|
||||
disabled={loading || isEmailPrefilled}
|
||||
readonly={isEmailPrefilled}
|
||||
autofocus
|
||||
aria-invalid={errors.email ? 'true' : undefined}
|
||||
autocomplete="email"
|
||||
|
||||
@@ -45,6 +45,8 @@ export const load = async ({ params, url }) => {
|
||||
course,
|
||||
currentOrg,
|
||||
invite: data.invite,
|
||||
inviteEmail: data.inviteContext?.recipientEmail ?? null,
|
||||
inviteEmailExists: data.inviteContext?.recipientExists ?? false,
|
||||
requiresPaymentOrInvite: false,
|
||||
token: inviteToken
|
||||
};
|
||||
|
||||
@@ -64,7 +64,12 @@
|
||||
const redirectUrl = `${redirectPath}${redirectSearch}`;
|
||||
|
||||
if (!$profile.id || !$profile.email) {
|
||||
goto(resolve(`/signup?redirect=${encodeURIComponent(redirectUrl)}`, {}));
|
||||
const inviteEmail = data.inviteEmail ?? '';
|
||||
const target = data.inviteEmailExists ? '/login' : '/signup';
|
||||
const params = new URLSearchParams({ redirect: redirectUrl });
|
||||
if (inviteEmail) params.set('email', inviteEmail);
|
||||
|
||||
goto(resolve(`${target}?${params.toString()}`, {}));
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import { AutomationPage } from '$features/automation/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{t.get('automation.title')} - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('automation.title')}</Page.Title>
|
||||
<Page.Subtitle>{$t('automation.subtitle')}</Page.Subtitle>
|
||||
</Page.HeaderContent>
|
||||
</Page.Header>
|
||||
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<AutomationPage />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
Reference in New Issue
Block a user