feat: structured artifact rendering, UX improvements

- Render structured artifacts from agent results with type-aware
  formatting: code blocks with syntax highlighting and copy button,
  terminal-style command output, search result cards, and text findings
- Make FinalResult panel collapsible (default collapsed) with scrollable
  content (max-h-96) to prevent dominating the chat view
- Add clickable URL detection in summaries and artifact content
- Fix code block contrast for both light and dark mode
- Animate progress bar with pulse ring on active step and gradient
  shimmer on connecting lines
- Fix tab-switching bug: use module-level orchestrationStore singleton
  so orchestration state survives route navigation
- Remove sample/demo data seeding and clean up persisted localStorage
  entries from previous sample sessions
- Remove showSampleBadge prop from PageHeader
- Regenerate proto types for new Artifact message and ArtifactType enum
- Update README project structure (remove deleted data/ directory)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shahondin1624
2026-03-12 23:13:33 +01:00
parent cfd338028a
commit 2c6c961e08
17 changed files with 456 additions and 346 deletions

View File

@@ -1,4 +1,4 @@
import { createClient } from '@connectrpc/connect';
import { createClient, ConnectError, Code } from '@connectrpc/connect';
import { createGrpcWebTransport } from '@connectrpc/connect-web';
import { OrchestratorService } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
import type {
@@ -9,6 +9,7 @@ import { create } from '@bufbuild/protobuf';
import { ProcessRequestRequestSchema } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
import { connectionStore } from '$lib/stores/connection.svelte';
import { toastStore } from '$lib/stores/toast.svelte';
import { logger } from '$lib/utils/logger';
/**
* Application-level error wrapping gRPC status codes.
@@ -112,10 +113,35 @@ function backoffDelay(attempt: number): number {
const MAX_RETRIES = 3;
/**
* Map numeric Code enum values to lowercase string names matching GRPC_USER_MESSAGES keys.
*/
const CODE_TO_STRING: Record<number, string> = {
[Code.Canceled]: 'cancelled',
[Code.Unknown]: 'unknown',
[Code.InvalidArgument]: 'failed_precondition',
[Code.DeadlineExceeded]: 'deadline_exceeded',
[Code.NotFound]: 'not_found',
[Code.AlreadyExists]: 'already_exists',
[Code.PermissionDenied]: 'permission_denied',
[Code.ResourceExhausted]: 'resource_exhausted',
[Code.FailedPrecondition]: 'failed_precondition',
[Code.Aborted]: 'aborted',
[Code.OutOfRange]: 'failed_precondition',
[Code.Unimplemented]: 'unimplemented',
[Code.Internal]: 'internal',
[Code.Unavailable]: 'unavailable',
[Code.DataLoss]: 'data_loss',
[Code.Unauthenticated]: 'unauthenticated'
};
/**
* Extract gRPC error code from an error, normalising to lowercase string.
*/
function extractCode(err: unknown): string {
if (err instanceof ConnectError) {
return CODE_TO_STRING[err.code] ?? 'unknown';
}
if (err instanceof Error && 'code' in err) {
const raw = (err as { code: unknown }).code;
if (typeof raw === 'string') return raw.toLowerCase();
@@ -129,6 +155,14 @@ function extractCode(err: unknown): string {
*/
function toOrchestratorError(err: unknown): OrchestratorError {
if (err instanceof OrchestratorError) return err;
if (err instanceof ConnectError) {
const code = extractCode(err);
const details = [
err.rawMessage,
err.cause ? String(err.cause) : ''
].filter(Boolean).join('; ');
return new OrchestratorError(friendlyMessage(code), code, details || undefined);
}
if (err instanceof Error) {
const code = extractCode(err);
return new OrchestratorError(friendlyMessage(code), code, err.message);
@@ -136,6 +170,13 @@ function toOrchestratorError(err: unknown): OrchestratorError {
return new OrchestratorError(friendlyMessage('unknown'), 'unknown');
}
/**
* Append a diagnostic code suffix to a message, e.g. "(code: unavailable)".
*/
function diagnosticSuffix(err: OrchestratorError): string {
return err.code && err.code !== 'unknown' ? ` (code: ${err.code})` : '';
}
/**
* Send a request to the orchestrator and yield streaming responses.
*
@@ -158,6 +199,11 @@ export async function* processRequest(
sessionConfig
});
logger.debug('orchestrator', 'processRequest', {
sessionId,
messageLength: userMessage.length
});
let lastError: OrchestratorError | null = null;
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
@@ -169,6 +215,11 @@ export async function* processRequest(
if (attempt > 0) {
connectionStore.setReconnecting();
const delay = backoffDelay(attempt - 1);
logger.warn('orchestrator', `Retry attempt ${attempt}/${MAX_RETRIES}`, {
sessionId,
previousCode: lastError?.code,
delay
});
await sleep(delay);
}
@@ -178,9 +229,11 @@ export async function* processRequest(
connectionStore.reportSuccess();
yield response;
}
logger.debug('orchestrator', 'Stream completed', { sessionId });
// Completed successfully — no retry needed
return;
} catch (err: unknown) {
logger.grpcError('orchestrator', `Request failed (attempt ${attempt + 1}/${MAX_RETRIES + 1})`, err);
lastError = toOrchestratorError(err);
const code = lastError.code;
@@ -192,7 +245,12 @@ export async function* processRequest(
// Non-transient or exhausted retries
connectionStore.reportFailure();
toastStore.addToast({ message: lastError.message, type: 'error' });
const suffix = diagnosticSuffix(lastError);
logger.error('orchestrator', 'Request failed permanently', {
code: lastError.code,
details: lastError.details
});
toastStore.addToast({ message: lastError.message + suffix, type: 'error' });
throw lastError;
}
}