diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..68c1f60 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,31 @@ +import { user } from '$lib/stores'; + +const API_KEY_STORAGE = 'nsct-api-key'; + +export function login(apiKey: string): Promise<{ userId: string; username: string }> { + setApiKey(apiKey); + // Set a minimal user object; real identity is fetched from the backend later + user.set({ apiKey }); + return Promise.resolve({ userId: apiKey, username: apiKey.substring(0, 8) }); +} + +export function logout(): void { + removeApiKey(); + user.set(null); +} + +export function isAuthenticated(): boolean { + return getApiKey() !== null; +} + +export function getApiKey(): string | null { + return localStorage.getItem(API_KEY_STORAGE); +} + +export function setApiKey(key: string): void { + localStorage.setItem(API_KEY_STORAGE, key); +} + +export function removeApiKey(): void { + localStorage.removeItem(API_KEY_STORAGE); +} \ No newline at end of file diff --git a/src/lib/components/ResearchListItem.svelte b/src/lib/components/ResearchListItem.svelte new file mode 100644 index 0000000..96a792f --- /dev/null +++ b/src/lib/components/ResearchListItem.svelte @@ -0,0 +1,44 @@ + + +
window.location.href = `/research/${item.id}`} +> +
+ + {item.id.slice(0, 8)} + + {formatDate(item.created_at)} +
+

+ {item.query} +

+
+ + {item.status} +
+
\ No newline at end of file diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte new file mode 100644 index 0000000..7b91fb3 --- /dev/null +++ b/src/lib/components/Sidebar.svelte @@ -0,0 +1,157 @@ + + + +
+ +
+ + +{#if isMobile && $sidebarOpen} +
+{/if} \ No newline at end of file diff --git a/src/lib/components/StatusBadge.svelte b/src/lib/components/StatusBadge.svelte new file mode 100644 index 0000000..ba10f95 --- /dev/null +++ b/src/lib/components/StatusBadge.svelte @@ -0,0 +1,35 @@ + + + + + {status?.status ?? 'N/A'} + \ No newline at end of file diff --git a/src/lib/components/TopBar.svelte b/src/lib/components/TopBar.svelte new file mode 100644 index 0000000..123e684 --- /dev/null +++ b/src/lib/components/TopBar.svelte @@ -0,0 +1,66 @@ + + +
+
+ + {#if title} +

{title}

+ {/if} +
+ +
+ {#if showToggle} + + {/if} + {#if isAuthenticated()} +
+ + +
+ {/if} +
+
\ No newline at end of file diff --git a/src/lib/sidebar.ts b/src/lib/sidebar.ts new file mode 100644 index 0000000..3e1bc8f --- /dev/null +++ b/src/lib/sidebar.ts @@ -0,0 +1,43 @@ +import { writable } from 'svelte/store'; +import { apiGet, apiDelete, apiPost } from '$lib/api'; +import type { ResearchSummary } from '$lib/types'; + +// Sidebar visibility (mobile toggle) +export const sidebarOpen = writable(false); + +// Currently selected research session ID +export const selectedResearchId = writable(null); + +export interface ResearchListItem extends ResearchSummary { + truncatedQuery?: string; + truncatedId?: string; +} + +const HISTORY_LIMIT = 50; + +export async function loadResearchHistory(apiKey: string): Promise { + const list = await apiGet('/v1/research', { apiKey }); + // Sort descending by created_at (newest first), limit to HISTORY_LIMIT + const sorted = list + .slice() + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + .slice(0, HISTORY_LIMIT); + return sorted; +} + +export async function deleteResearch(id: string, apiKey: string): Promise { + await apiDelete(`/v1/research/${id}`, { apiKey }); +} + +export async function createResearch( + apiKey: string, + query: string, + language: string, + depth: string +): Promise<{ id: string }> { + return await apiPost<{ id: string }>( + '/v1/research', + { query, language, depth }, + { apiKey } + ); +} \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index d41435e..aa91ea4 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -38,4 +38,19 @@ export interface EvidenceScore { contradiction_level: number; evidence_directness: number; date_relevance: number; -} \ No newline at end of file +} + +export interface ResearchDetail { + id: string; + query: string; + status: string; + progress?: number; + created_at: string; + completed_at?: string; + error?: string; + sources?: SourceInfo[]; + claims?: ClaimInfo[]; + evidence?: EvidenceScore[]; + report?: string; + methodology?: string; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 92d97fb..3056bba 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,49 +1,114 @@ - -
+
-

NSCT

-

Neutral Search Crawler Tool

- -
-

Anmelden

- e.key === 'Enter' && login()} - /> - + +
+

NSCT

+

Neutral Search Crawler Tool

+
+ + +
+

Anmelden

+ +
+
+ + +
+ + +
+ {#if error} -

{error}

+

{error}

{/if}
- - + + +

+ Noch keinen Account? API-Key vom Admin anfordern +

+ + +
+ +
\ No newline at end of file diff --git a/src/routes/research/+layout.svelte b/src/routes/research/+layout.svelte new file mode 100644 index 0000000..5fdef0f --- /dev/null +++ b/src/routes/research/+layout.svelte @@ -0,0 +1,20 @@ + + +
+ + + + +
+ + +
+
\ No newline at end of file diff --git a/src/routes/research/new/+page.svelte b/src/routes/research/new/+page.svelte new file mode 100644 index 0000000..57693d5 --- /dev/null +++ b/src/routes/research/new/+page.svelte @@ -0,0 +1,145 @@ + + +
+
+

Neue Recherche

+ +
+ +
+ + +
+ + +
+ +
+ {#each ['Deutsch', 'English', 'Multi'] as lang} + + {/each} +
+
+ + +
+ +
+ {#each ['quick', 'normal', 'deep'] as d} + + {/each} +
+
+ + + +
+ + + {#if error} +
+

{error}

+
+ {/if} +
+
\ No newline at end of file diff --git a/src/routes/research/{id}/+page.svelte b/src/routes/research/{id}/+page.svelte new file mode 100644 index 0000000..adf1792 --- /dev/null +++ b/src/routes/research/{id}/+page.svelte @@ -0,0 +1,293 @@ + + +
+ +
+
+

+ {detail?.query ?? detail?.id ?? researchId} +

+
+ {#if status} + + + {status.status} + + {#if status.progress !== undefined} + {status.progress}% + {/if} + {/if} +
+
+
+ + + {#if error} +
+

⚠ {error}

+
+ {/if} + + +
+ +
+ + +
+ + {#if activeTab === 'Status'} +
+

Aktueller Status

+ {#if status} +
+
+ Status + + {status.status} + +
+ {#if status.progress !== undefined} +
+
+ Fortschritt + {status.progress}% +
+
+
+
+
+ {/if} +
+ Letzte Aktualisierung + {lastUpdate} +
+ {#if status.error} +
+

Fehler: {status.error}

+
+ {/if} +
+ {:else} +

Status wird geladen…

+ {/if} +
+ + + {:else if activeTab === 'Quellen'} +
+

Gefundene Quellen

+ {#if detail?.sources && detail.sources.length > 0} +
+ {#each detail.sources as source} +
+

{source.title}

+

{source.url}

+ {#if source.domain} + {source.domain} + {/if} +
+ {/each} +
+ {:else} +

Noch keine Quellen gefunden.

+ {/if} +
+ + + {:else if activeTab === 'Claims'} +
+

Extrahierte Claims

+ {#if detail?.claims && detail.claims.length > 0} +
+ {#each detail.claims as claim} +
+

{claim.claim}

+
+ Typ: {claim.claim_type} + Confidence: {(claim.confidence * 100).toFixed(0)}% +
+
+ {/each} +
+ {:else} +

Noch keine Claims extrahiert.

+ {/if} +
+ + + {:else if activeTab === 'Evidence'} +
+

Evidence-Scores

+ {#if detail?.evidence && detail.evidence.length > 0} +
+ {#each detail.evidence as score, i} +
+

Score {i + 1}

+
+
+ Quellen-Unabhängigkeit + {(score.source_independence * 100).toFixed(0)}% +
+
+ Primärquellen-Nähe + {(score.primary_source_proximity * 100).toFixed(0)}% +
+
+ Quellen-Übereinstimmung + {(score.cross_source_support * 100).toFixed(0)}% +
+
+ Widerspruch + {(score.contradiction_level * 100).toFixed(0)}% +
+
+ Evidenz-Direktheit + {(score.evidence_directness * 100).toFixed(0)}% +
+
+ Datumsrelevanz + {(score.date_relevance * 100).toFixed(0)}% +
+
+
+ {/each} +
+ {:else} +

Noch keine Evidence-Scores berechnet.

+ {/if} +
+ + + {:else if activeTab === 'Bericht'} +
+

Forschungsbericht

+ {#if detail?.report} +
{detail.report}
+ {:else} +

Bericht wird generiert, sobald die Recherche abgeschlossen ist.

+ {/if} +
+ + + {:else if activeTab === 'Methodik'} +
+

Methodik-Dokumentation

+ {#if detail?.methodology} +
{detail.methodology}
+ {:else} +

Methodik wird nach Abschluss der Recherche verfügbar sein.

+ {/if} +
+ {/if} +
+
\ No newline at end of file