Add research planning detail tab
This commit is contained in:
@@ -50,6 +50,11 @@ interface ApiReportResponse {
|
||||
methodology: string;
|
||||
}
|
||||
|
||||
interface ApiPlanResponse {
|
||||
plan: ResearchPlan | null;
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
function normalizeStatus(status: ApiResearchStatus): ResearchStatus {
|
||||
return {
|
||||
id: status.research_id,
|
||||
@@ -103,12 +108,13 @@ export async function getResearchDetail(
|
||||
apiKey: string,
|
||||
researchId: string
|
||||
): Promise<ResearchDetail> {
|
||||
const [status, sources, claims, evidence, report] = await Promise.all([
|
||||
const [status, sources, claims, evidence, report, plan] = await Promise.all([
|
||||
apiGet<ApiResearchStatus>(`/v1/research/${researchId}`, { apiKey }),
|
||||
apiGet<ApiSourcesResponse>(`/v1/research/${researchId}/sources`, { apiKey }),
|
||||
apiGet<ApiClaimsResponse>(`/v1/research/${researchId}/claims`, { apiKey }),
|
||||
apiGet<ApiEvidenceResponse>(`/v1/research/${researchId}/evidence`, { apiKey }),
|
||||
apiGet<ApiReportResponse>(`/v1/research/${researchId}/report`, { apiKey }),
|
||||
apiGet<ApiPlanResponse>(`/v1/research/${researchId}/plan`, { apiKey }),
|
||||
]);
|
||||
return {
|
||||
...normalizeDetail(status),
|
||||
@@ -136,6 +142,7 @@ export async function getResearchDetail(
|
||||
.filter(Boolean)
|
||||
.join('\n\n'),
|
||||
methodology: report.methodology,
|
||||
plan: plan.available ? plan.plan ?? undefined : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -246,9 +253,23 @@ export interface ResearchDetail {
|
||||
evidence_scores?: Record<string, number>;
|
||||
report?: string;
|
||||
methodology?: string;
|
||||
plan?: ResearchPlan;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface ResearchPlan {
|
||||
topic: string;
|
||||
time_range?: { start?: string | null; end?: string | null; description?: string };
|
||||
entities?: string[];
|
||||
search_dimensions?: string[];
|
||||
queries?: Array<{ query: string; purpose: string; category: string; language: string }>;
|
||||
potential_sources?: Array<{ type: string; description: string }>;
|
||||
counter_hypotheses?: string[];
|
||||
search_bias_mitigation?: string[];
|
||||
estimated_depth?: string;
|
||||
confidence?: number;
|
||||
}
|
||||
|
||||
export interface ShareData {
|
||||
id: string;
|
||||
query: string;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import EvidenceTab from './components/EvidenceTab.svelte';
|
||||
import ReportTab from './components/ReportTab.svelte';
|
||||
import MethodologyTab from './components/MethodologyTab.svelte';
|
||||
import PlanTab from './components/PlanTab.svelte';
|
||||
import ShareModal from '$lib/components/ShareModal.svelte';
|
||||
import ShareButton from './components/ShareButton.svelte';
|
||||
import { errorBanners } from '$lib/components/ErrorBannerStore';
|
||||
@@ -29,8 +30,8 @@
|
||||
let hasFinished = false;
|
||||
|
||||
// --- Tab definitions with i18n labels ---
|
||||
const TABS = ['Status', 'Quellen', 'Claims', 'Evidence', 'Bericht', 'Methodik'];
|
||||
const TABS_COMPLETED = ['Quellen', 'Claims', 'Evidence', 'Bericht', 'Methodik'];
|
||||
const TABS = ['Status', 'Planung', 'Quellen', 'Claims', 'Evidence', 'Bericht', 'Methodik'];
|
||||
const TABS_COMPLETED = ['Planung', 'Quellen', 'Claims', 'Evidence', 'Bericht', 'Methodik'];
|
||||
|
||||
// --- Polling ---
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
@@ -279,6 +280,10 @@
|
||||
completed_at={detail?.completed_at ?? ''}
|
||||
/>
|
||||
|
||||
<!-- Planning Tab -->
|
||||
{:else if activeTab === 'Planung'}
|
||||
<PlanTab plan={detail?.plan} loading={false} {error} />
|
||||
|
||||
<!-- Sources Tab -->
|
||||
{:else if activeTab === 'Quellen'}
|
||||
<SourcesTab
|
||||
|
||||
100
src/routes/research/[id]/components/PlanTab.svelte
Normal file
100
src/routes/research/[id]/components/PlanTab.svelte
Normal file
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import type { ResearchPlan } from '$lib/research-api';
|
||||
|
||||
export let plan: ResearchPlan | undefined;
|
||||
export let loading = false;
|
||||
export let error: string | null = null;
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
primary_source: 'Primärquelle',
|
||||
news: 'Nachrichten',
|
||||
scientific: 'Wissenschaft',
|
||||
counter_evidence: 'Gegenprüfung',
|
||||
general: 'Allgemein'
|
||||
};
|
||||
|
||||
const sourceLabels: Record<string, string> = {
|
||||
primary_source: 'Primärquelle',
|
||||
secondary_source: 'Sekundärquelle',
|
||||
academic: 'Wissenschaftliche Quelle'
|
||||
};
|
||||
|
||||
function label(values: Record<string, string>, value: string): string {
|
||||
return values[value] ?? value;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Planungsinformationen werden geladen …</p>
|
||||
{:else if error}
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
{:else if !plan}
|
||||
<div class="rounded-lg border border-gray-200 dark:border-gray-700 p-5 text-sm text-gray-500 dark:text-gray-400">
|
||||
Der validierte Rechercheplan ist noch nicht verfügbar. Er erscheint nach Abschluss der Planungsphase.
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-6 text-sm text-gray-700 dark:text-gray-200">
|
||||
<section>
|
||||
<h2 class="text-base font-semibold text-gray-900 dark:text-white">{plan.topic}</h2>
|
||||
<p class="mt-1 text-gray-500 dark:text-gray-400">
|
||||
Zeitraum: {plan.time_range?.description ?? 'Nicht festgelegt'}
|
||||
{#if plan.estimated_depth} · Tiefe: {plan.estimated_depth}{/if}
|
||||
{#if plan.confidence !== undefined} · Planungszuversicht: {Math.round(plan.confidence * 100)}%{/if}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Suchanfragen</h3>
|
||||
<div class="mt-2 space-y-2">
|
||||
{#each plan.queries ?? [] as query}
|
||||
<article class="rounded-lg border border-gray-200 dark:border-gray-700 p-3">
|
||||
<p class="font-medium text-gray-900 dark:text-white">{query.query}</p>
|
||||
<p class="mt-1 text-gray-500 dark:text-gray-400">{query.purpose}</p>
|
||||
<span class="mt-2 inline-block rounded bg-indigo-50 px-2 py-0.5 text-xs text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-200">
|
||||
{label(categoryLabels, query.category)} · {query.language}
|
||||
</span>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-5 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Perspektiven und Entitäten</h3>
|
||||
<ul class="mt-2 list-disc space-y-1 pl-5">
|
||||
{#each plan.search_dimensions ?? [] as dimension}<li>{dimension}</li>{/each}
|
||||
{#each plan.entities ?? [] as entity}<li>{entity}</li>{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Vorgesehene Quellen</h3>
|
||||
<ul class="mt-2 space-y-2">
|
||||
{#each plan.potential_sources ?? [] as source}
|
||||
<li><span class="font-medium">{label(sourceLabels, source.type)}:</span> {source.description}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if plan.counter_hypotheses?.length || plan.search_bias_mitigation?.length}
|
||||
<section class="grid gap-5 md:grid-cols-2">
|
||||
{#if plan.counter_hypotheses?.length}
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Gegenhypothesen</h3>
|
||||
<ul class="mt-2 list-disc space-y-1 pl-5">
|
||||
{#each plan.counter_hypotheses as hypothesis}<li>{hypothesis}</li>{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{#if plan.search_bias_mitigation?.length}
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Bias-Minderung</h3>
|
||||
<ul class="mt-2 list-disc space-y-1 pl-5">
|
||||
{#each plan.search_bias_mitigation as measure}<li>{measure}</li>{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user