forked from 0xWheatyz/SPARC
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0ed39908e | |||
| 87e09b365b | |||
| 5d11f514c0 | |||
| cbc8f449a1 |
@@ -479,6 +479,20 @@ SUPPORTED_MODELS = [
|
|||||||
{"id": "meta-llama/llama-3.1-70b-instruct", "name": "Llama 3.1 70B", "provider": "Meta"},
|
{"id": "meta-llama/llama-3.1-70b-instruct", "name": "Llama 3.1 70B", "provider": "Meta"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
_SUPPORTED_MODEL_IDS = {m["id"] for m in SUPPORTED_MODELS}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_model(model: str | None) -> None:
|
||||||
|
"""Raise HTTP 400 if *model* is not in the supported allow-list."""
|
||||||
|
if model is not None and model not in _SUPPORTED_MODEL_IDS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=(
|
||||||
|
f"Unsupported model '{model}'. "
|
||||||
|
f"Supported models: {', '.join(sorted(_SUPPORTED_MODEL_IDS))}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/models", tags=["System"])
|
@app.get("/models", tags=["System"])
|
||||||
async def list_models():
|
async def list_models():
|
||||||
@@ -814,6 +828,7 @@ async def analyze_company(
|
|||||||
Returns:
|
Returns:
|
||||||
Analysis results including patent count, AI insights, and success status
|
Analysis results including patent count, AI insights, and success status
|
||||||
"""
|
"""
|
||||||
|
_validate_model(model)
|
||||||
if not _analyzer:
|
if not _analyzer:
|
||||||
raise HTTPException(status_code=503, detail="Analyzer not initialized")
|
raise HTTPException(status_code=503, detail="Analyzer not initialized")
|
||||||
|
|
||||||
@@ -873,6 +888,7 @@ async def analyze_companies_batch(
|
|||||||
Returns:
|
Returns:
|
||||||
Batch results with individual company analyses and summary statistics
|
Batch results with individual company analyses and summary statistics
|
||||||
"""
|
"""
|
||||||
|
_validate_model(request.model)
|
||||||
if not _analyzer:
|
if not _analyzer:
|
||||||
raise HTTPException(status_code=503, detail="Analyzer not initialized")
|
raise HTTPException(status_code=503, detail="Analyzer not initialized")
|
||||||
|
|
||||||
@@ -983,6 +999,7 @@ async def analyze_companies_async(
|
|||||||
Returns:
|
Returns:
|
||||||
Job status with job_id for polling
|
Job status with job_id for polling
|
||||||
"""
|
"""
|
||||||
|
_validate_model(request.model)
|
||||||
global _job_counter
|
global _job_counter
|
||||||
|
|
||||||
_job_counter += 1
|
_job_counter += 1
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { useTheme } from './ThemeContext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns theme-aware color values for recharts components.
|
||||||
|
*
|
||||||
|
* Recharts accepts only raw color strings (not CSS variables),
|
||||||
|
* so this hook bridges the Tailwind/CSS-variable theme system
|
||||||
|
* to the imperative recharts API.
|
||||||
|
*/
|
||||||
|
export function useChartTheme() {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** Axis tick and grid line stroke color */
|
||||||
|
axisStroke: isDark ? '#94a3b8' : '#64748b',
|
||||||
|
/** Tooltip container background */
|
||||||
|
tooltipBg: isDark ? '#1e293b' : '#ffffff',
|
||||||
|
/** Tooltip container border */
|
||||||
|
tooltipBorder: isDark
|
||||||
|
? '1px solid rgba(99, 102, 241, 0.3)'
|
||||||
|
: '1px solid rgba(99, 102, 241, 0.2)',
|
||||||
|
/** Tooltip label text color */
|
||||||
|
tooltipLabelColor: isDark ? '#f8fafc' : '#0f172a',
|
||||||
|
/** Tooltip item text color */
|
||||||
|
tooltipItemColor: isDark ? '#e2e8f0' : '#334155',
|
||||||
|
/** Convenience: full contentStyle object for recharts Tooltip */
|
||||||
|
tooltipContentStyle: {
|
||||||
|
backgroundColor: isDark ? '#1e293b' : '#ffffff',
|
||||||
|
border: isDark
|
||||||
|
? '1px solid rgba(99, 102, 241, 0.3)'
|
||||||
|
: '1px solid rgba(99, 102, 241, 0.2)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: isDark ? '#f8fafc' : '#0f172a',
|
||||||
|
},
|
||||||
|
/** Convenience: labelStyle for recharts Tooltip */
|
||||||
|
tooltipLabelStyle: {
|
||||||
|
color: isDark ? '#f8fafc' : '#0f172a',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,11 +3,13 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { analyticsApi } from '../api/client';
|
import { analyticsApi } from '../api/client';
|
||||||
import { AlertCircle, Database } from 'lucide-react';
|
import { AlertCircle, Database } from 'lucide-react';
|
||||||
import { PieChart, Pie, Cell, BarChart, Bar, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
import { PieChart, Pie, Cell, BarChart, Bar, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
||||||
|
import { useChartTheme } from '../context/useChartTheme';
|
||||||
|
|
||||||
const COLORS = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'];
|
const COLORS = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'];
|
||||||
|
|
||||||
export function AnalyticsPage() {
|
export function AnalyticsPage() {
|
||||||
const [days, setDays] = useState(30);
|
const [days, setDays] = useState(30);
|
||||||
|
const chartTheme = useChartTheme();
|
||||||
|
|
||||||
const { data, isLoading, isError, refetch } = useQuery({
|
const { data, isLoading, isError, refetch } = useQuery({
|
||||||
queryKey: ['analytics', days],
|
queryKey: ['analytics', days],
|
||||||
@@ -160,11 +162,7 @@ export function AnalyticsPage() {
|
|||||||
))}
|
))}
|
||||||
</Pie>
|
</Pie>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={chartTheme.tooltipContentStyle}
|
||||||
backgroundColor: '#1e293b',
|
|
||||||
border: '1px solid rgba(99, 102, 241, 0.3)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
</PieChart>
|
</PieChart>
|
||||||
@@ -178,15 +176,11 @@ export function AnalyticsPage() {
|
|||||||
<h3 className="text-lg font-semibold text-text-primary mb-4">Analysis Types</h3>
|
<h3 className="text-lg font-semibold text-text-primary mb-4">Analysis Types</h3>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<BarChart data={typeData}>
|
<BarChart data={typeData}>
|
||||||
<XAxis dataKey="name" stroke="#94a3b8" fontSize={12} />
|
<XAxis dataKey="name" stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<YAxis stroke="#94a3b8" fontSize={12} />
|
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={chartTheme.tooltipContentStyle}
|
||||||
backgroundColor: '#1e293b',
|
labelStyle={chartTheme.tooltipLabelStyle}
|
||||||
border: '1px solid rgba(99, 102, 241, 0.3)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
}}
|
|
||||||
labelStyle={{ color: '#f8fafc' }}
|
|
||||||
/>
|
/>
|
||||||
<Bar dataKey="count" fill="#6366f1" radius={[4, 4, 0, 0]} />
|
<Bar dataKey="count" fill="#6366f1" radius={[4, 4, 0, 0]} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
@@ -222,15 +216,11 @@ export function AnalyticsPage() {
|
|||||||
<h4 className="text-md font-semibold text-text-primary mb-4">Analyses per Company Over Time</h4>
|
<h4 className="text-md font-semibold text-text-primary mb-4">Analyses per Company Over Time</h4>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<LineChart data={pivoted}>
|
<LineChart data={pivoted}>
|
||||||
<XAxis dataKey="month" stroke="#94a3b8" fontSize={12} />
|
<XAxis dataKey="month" stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<YAxis stroke="#94a3b8" fontSize={12} />
|
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={chartTheme.tooltipContentStyle}
|
||||||
backgroundColor: '#1e293b',
|
labelStyle={chartTheme.tooltipLabelStyle}
|
||||||
border: '1px solid rgba(99, 102, 241, 0.3)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
}}
|
|
||||||
labelStyle={{ color: '#f8fafc' }}
|
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
{companies.map((company, idx) => (
|
{companies.map((company, idx) => (
|
||||||
@@ -268,15 +258,11 @@ export function AnalyticsPage() {
|
|||||||
<h4 className="text-md font-semibold text-text-primary mb-4">Analysis Types Over Time</h4>
|
<h4 className="text-md font-semibold text-text-primary mb-4">Analysis Types Over Time</h4>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<BarChart data={pivoted}>
|
<BarChart data={pivoted}>
|
||||||
<XAxis dataKey="month" stroke="#94a3b8" fontSize={12} />
|
<XAxis dataKey="month" stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<YAxis stroke="#94a3b8" fontSize={12} />
|
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={chartTheme.tooltipContentStyle}
|
||||||
backgroundColor: '#1e293b',
|
labelStyle={chartTheme.tooltipLabelStyle}
|
||||||
border: '1px solid rgba(99, 102, 241, 0.3)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
}}
|
|
||||||
labelStyle={{ color: '#f8fafc' }}
|
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
{types.map((type, idx) => (
|
{types.map((type, idx) => (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useMutation, useQuery } from '@tanstack/react-query';
|
|||||||
import { analysisApi } from '../api/client';
|
import { analysisApi } from '../api/client';
|
||||||
import { Rocket, CheckCircle, AlertCircle, ChevronDown, ChevronUp, RefreshCw, Inbox } from 'lucide-react';
|
import { Rocket, CheckCircle, AlertCircle, ChevronDown, ChevronUp, RefreshCw, Inbox } from 'lucide-react';
|
||||||
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
|
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
|
||||||
|
import { useChartTheme } from '../context/useChartTheme';
|
||||||
import type { BatchAnalysisResult } from '../types';
|
import type { BatchAnalysisResult } from '../types';
|
||||||
|
|
||||||
export function Batch() {
|
export function Batch() {
|
||||||
@@ -12,6 +13,8 @@ export function Batch() {
|
|||||||
const [result, setResult] = useState<BatchAnalysisResult | null>(null);
|
const [result, setResult] = useState<BatchAnalysisResult | null>(null);
|
||||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const chartTheme = useChartTheme();
|
||||||
|
|
||||||
const modelsQuery = useQuery({
|
const modelsQuery = useQuery({
|
||||||
queryKey: ['models'],
|
queryKey: ['models'],
|
||||||
queryFn: () => analysisApi.listModels(),
|
queryFn: () => analysisApi.listModels(),
|
||||||
@@ -210,15 +213,11 @@ export function Batch() {
|
|||||||
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6">
|
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6">
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<BarChart data={chartData}>
|
<BarChart data={chartData}>
|
||||||
<XAxis dataKey="name" stroke="#94a3b8" fontSize={12} />
|
<XAxis dataKey="name" stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<YAxis stroke="#94a3b8" fontSize={12} />
|
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={chartTheme.tooltipContentStyle}
|
||||||
backgroundColor: '#1e293b',
|
labelStyle={chartTheme.tooltipLabelStyle}
|
||||||
border: '1px solid rgba(99, 102, 241, 0.3)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
}}
|
|
||||||
labelStyle={{ color: '#f8fafc' }}
|
|
||||||
/>
|
/>
|
||||||
<Bar dataKey="patents" radius={[4, 4, 0, 0]}>
|
<Bar dataKey="patents" radius={[4, 4, 0, 0]}>
|
||||||
{chartData.map((entry, index) => (
|
{chartData.map((entry, index) => (
|
||||||
|
|||||||
@@ -182,3 +182,47 @@ class TestJobEndpoints:
|
|||||||
"""Test listing jobs with status filter."""
|
"""Test listing jobs with status filter."""
|
||||||
response = client.get("/jobs?status=completed")
|
response = client.get("/jobs?status=completed")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
class TestModelValidation:
|
||||||
|
"""Test that unsupported model identifiers are rejected."""
|
||||||
|
|
||||||
|
def test_analyze_rejects_unsupported_model(self, client, mock_analyzer):
|
||||||
|
"""GET /analyze/{company} with unsupported model returns 400."""
|
||||||
|
response = client.get("/analyze/nvidia?model=fake/nonexistent-model")
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "Unsupported model" in response.json()["detail"]
|
||||||
|
|
||||||
|
def test_analyze_accepts_supported_model(self, client, mock_analyzer):
|
||||||
|
"""GET /analyze/{company} with a supported model succeeds."""
|
||||||
|
mock_result = CompanyAnalysisResult(
|
||||||
|
company_name="nvidia",
|
||||||
|
analysis="test",
|
||||||
|
patent_count=1,
|
||||||
|
success=True,
|
||||||
|
timestamp=datetime.now(),
|
||||||
|
model="anthropic/claude-3.5-sonnet",
|
||||||
|
)
|
||||||
|
mock_analyzer._analyze_company_safe.return_value = mock_result
|
||||||
|
|
||||||
|
response = client.get("/analyze/nvidia?model=anthropic/claude-3.5-sonnet")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
def test_batch_rejects_unsupported_model(self, client, mock_analyzer):
|
||||||
|
"""POST /analyze/batch with unsupported model returns 400."""
|
||||||
|
response = client.post(
|
||||||
|
"/analyze/batch",
|
||||||
|
json={"companies": ["nvidia"], "model": "fake/nonexistent-model"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "Unsupported model" in response.json()["detail"]
|
||||||
|
|
||||||
|
def test_list_models_returns_supported(self, client):
|
||||||
|
"""GET /models returns the allow-list."""
|
||||||
|
response = client.get("/models")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "models" in data
|
||||||
|
assert "default" in data
|
||||||
|
assert len(data["models"]) > 0
|
||||||
|
assert all("id" in m and "name" in m and "provider" in m for m in data["models"])
|
||||||
|
|||||||
Reference in New Issue
Block a user