FE-3 + FE-4: Full-Featured Research-Ansicht + Docker Deployment
FE-3:
- Error-System: ErrorBannerStore + ErrorBanner (stapelbar, auto-hide, 5 categories)
- ContradictionBanner: Visuelle Widerspruchserkennung
- StatusTab: Zeitleiste, Fortschrittsbalken, Details
- ShareButton: Inline-Share mit Geteilt-Badge
- ShareModal: Ablaufzeit (24h-30Tage/Nie), Max-Views, Copy/Revoke
- research/{id}/+page.svelte: 6 Tabs mit Polling (5s), onDestroy cleanup
- share/[token]/+page.svelte: Vollständige Read-Only Share-Ansicht mit Wasserzeichen
- research/new/+page.svelte: Char-Counter, Language/Depth-Auswahl
FE-4:
- Dockerfile: HEALTHCHECK, Error-Handling, multi-stage build
- Caddyfile: HTTPS, Security Headers, gzip+zstd
- docker-compose.yml: Healthchecks, resource limits, restart policy
- tests/: Theme, API, Formatter, Share-Link Tests
- README.md: Architektur-Diagramm, Installation, Nutzung
- CHANGELOG.md: FE-0 bis FE-4
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { isAuthenticated } from '$lib/auth';
|
||||
import { createResearch } from '$lib/sidebar';
|
||||
import { errorBanners } from '$lib/components/ErrorBannerStore';
|
||||
|
||||
let query = '';
|
||||
let language = 'Deutsch';
|
||||
@@ -9,6 +10,12 @@
|
||||
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');
|
||||
}
|
||||
@@ -16,12 +23,20 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -30,9 +45,12 @@
|
||||
|
||||
try {
|
||||
const result = await createResearch(key, query.trim(), language, depth);
|
||||
errorBanners.addErrorSuccess('Research wird erstellt…');
|
||||
window.location.href = `/research/${result.id}`;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Erstellen der Recherche.';
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Erstellen der Recherche.';
|
||||
error = msg;
|
||||
errorBanners.addError(msg, 'error');
|
||||
} finally {
|
||||
submitting = false;
|
||||
}
|
||||
@@ -44,7 +62,7 @@
|
||||
<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 -->
|
||||
<!-- Query textarea with counter -->
|
||||
<div>
|
||||
<label for="query" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Suchanfrage
|
||||
@@ -52,11 +70,20 @@
|
||||
<textarea
|
||||
id="query"
|
||||
bind:value={query}
|
||||
placeholder="Gib eine Suchanfrage ein..."
|
||||
rows="6"
|
||||
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 -->
|
||||
@@ -64,13 +91,14 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Sprache
|
||||
</label>
|
||||
<div class="flex gap-3">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each ['Deutsch', 'English', 'Multi'] as lang}
|
||||
<label
|
||||
class="flex items-center gap-2 px-4 py-2 border rounded-lg cursor-pointer transition-colors"
|
||||
class="flex items-center gap-2 px-4 py-2.5 border rounded-lg cursor-pointer transition-colors"
|
||||
class:ring-2 class:ring-indigo-500={language === lang}
|
||||
class:border-indigo-500={language === lang}
|
||||
class:border-gray-300 dark:border-gray-600={language !== lang}
|
||||
class:bg-indigo-50 dark:class:bg-indigo-900/20={language === lang}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
@@ -91,13 +119,14 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Tiefe
|
||||
</label>
|
||||
<div class="flex gap-3">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each ['quick', 'normal', 'deep'] as d}
|
||||
<label
|
||||
class="flex items-center gap-2 px-4 py-2 border rounded-lg cursor-pointer transition-colors"
|
||||
class="flex items-center gap-2 px-4 py-2.5 border rounded-lg cursor-pointer transition-colors"
|
||||
class:ring-2 class:ring-indigo-500={depth === d}
|
||||
class:border-indigo-500={depth === d}
|
||||
class:border-gray-300 dark:border-gray-600={depth !== d}
|
||||
class:bg-indigo-50 dark:class:bg-indigo-900/20={depth === d}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
@@ -108,7 +137,7 @@
|
||||
disabled={submitting}
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{d === 'quick' ? 'Schnell' : d === 'normal' ? 'Normal' : 'Tief'}
|
||||
{d === 'quick' ? '⚡ Schnell' : d === 'normal' ? '📋 Normal' : '🔍 Tief'}
|
||||
</span>
|
||||
</label>
|
||||
{/each}
|
||||
@@ -118,8 +147,8 @@
|
||||
<!-- Submit button -->
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
class="w-full px-6 py-3 bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:cursor-not-allowed"
|
||||
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">
|
||||
@@ -127,7 +156,7 @@
|
||||
<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>
|
||||
Recherche wird gestartet…
|
||||
Wird erstellt…
|
||||
</span>
|
||||
{:else}
|
||||
Recherche starten
|
||||
|
||||
Reference in New Issue
Block a user