Files
NSCT-FrontEnd/src/routes/research/new/+page.svelte
faligam 8eb0383d48 Fix API-key login and research navigation
Validate submitted keys against the protected research endpoint instead of the nonexistent versioned health path. Replace server-only redirects in client code with SvelteKit navigation and route successful logins to the existing new-research screen.

Align frontend research history and creation calls with the backend response contract: list items are wrapped in items and new runs return research_id.
2026-09-06 17:23:11 +02:00

166 lines
7.2 KiB
Svelte

<script lang="ts">
import { isAuthenticated } from '$lib/auth';
import { createResearch } from '$lib/sidebar';
import { errorBanners } from '$lib/components/ErrorBannerStore';
let query = '';
let language = 'Deutsch';
let depth = 'normal';
let submitting = false;
let error = '';
const MAX_LENGTH = 500;
$: charCount = query.length;
$: remaining = MAX_LENGTH - charCount;
$: charCountColor = remaining < 0 ? 'text-red-500' : remaining <= 20 ? 'text-amber-500' : 'text-gray-400';
function getApiKey(): string | null {
return localStorage.getItem('nsct-api-key');
}
async function handleSubmit() {
if (!query.trim()) {
error = 'Bitte gib eine Suchanfrage ein.';
errorBanners.addError('Keine Suchanfrage eingegeben.', 'warning');
return;
}
if (query.length > MAX_LENGTH) {
error = `Maximale Länge von ${MAX_LENGTH} Zeichen erreicht.`;
errorBanners.addError('Zu lange Anfrage.', 'warning');
return;
}
const key = getApiKey();
if (!key) {
error = 'Nicht authentifiziert.';
errorBanners.addError('Kein API-Key verfügbar.', 'error');
return;
}
submitting = true;
error = '';
try {
const result = await createResearch(key, query.trim(), language, depth);
errorBanners.addErrorSuccess('Research wird erstellt…');
window.location.href = `/research/${result.research_id}`;
} catch (e) {
const msg = e instanceof Error ? e.message : 'Fehler beim Erstellen der Recherche.';
error = msg;
errorBanners.addError(msg, 'error');
} finally {
submitting = false;
}
}
</script>
<div class="flex items-center justify-center min-h-[calc(100vh-4rem)] px-4 py-8">
<div class="w-full max-w-2xl">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Neue Recherche</h1>
<form on:submit|preventDefault={handleSubmit} class="space-y-4">
<!-- Query textarea with counter -->
<div>
<label for="query" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Suchanfrage
</label>
<textarea
id="query"
bind:value={query}
placeholder="Gib eine Suchanfrage ein... (z.B. 'Was sind die neuesten Forschungsergebnisse zu KI in der Medizin?')"
rows="5"
maxlength={MAX_LENGTH}
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 focus:border-transparent resize-none transition-colors"
disabled={submitting}
></textarea>
<div class="flex items-center justify-between mt-1">
<p class="text-xs text-gray-500 dark:text-gray-400">
Mindestens 5 Zeichen erforderlich.
</p>
<p class="text-xs font-medium {charCountColor}">
{charCount} / {MAX_LENGTH}
</p>
</div>
</div>
<!-- Language selection -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Sprache
</label>
<div class="flex flex-wrap gap-2">
{#each ['Deutsch', 'English', 'Multi'] as lang}
<label
class="flex items-center gap-2 px-4 py-2.5 border rounded-lg cursor-pointer transition-colors {language === lang ? 'ring-2 ring-indigo-500 border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20' : 'border-gray-300 dark:border-gray-600'}"
>
<input
type="radio"
name="language"
value={lang}
bind:group={language}
class="sr-only"
disabled={submitting}
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{lang}</span>
</label>
{/each}
</div>
</div>
<!-- Depth selection -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tiefe
</label>
<div class="flex flex-wrap gap-2">
{#each ['quick', 'normal', 'deep'] as d}
<label
class="flex items-center gap-2 px-4 py-2.5 border rounded-lg cursor-pointer transition-colors {depth === d ? 'ring-2 ring-indigo-500 border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20' : 'border-gray-300 dark:border-gray-600'}"
>
<input
type="radio"
name="depth"
value={d}
bind:group={depth}
class="sr-only"
disabled={submitting}
/>
<span class="text-sm text-gray-700 dark:text-gray-300">
{d === 'quick' ? '⚡ Schnell' : d === 'normal' ? '📋 Normal' : '🔍 Tief'}
</span>
</label>
{/each}
</div>
</div>
<!-- Submit button -->
<button
type="submit"
disabled={submitting || query.length < 5}
class="w-full px-6 py-3 bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
{#if submitting}
<span class="flex items-center justify-center gap-2">
<svg class="animate-spin h-5 w-5" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
Wird erstellt…
</span>
{:else}
Recherche starten
{/if}
</button>
</form>
<!-- Error message -->
{#if error}
<div class="mt-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p class="text-sm text-red-700 dark:text-red-300">{error}</p>
</div>
{/if}
</div>
</div>