UI: add JobHistory component and render on stock detail page\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
ticker: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function JobHistory({ ticker }: Props) {
|
||||||
|
const [jobs, setJobs] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selected, setSelected] = useState<any | null>(null);
|
||||||
|
|
||||||
|
const fetchJobs = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/jobs?ticker=${encodeURIComponent(ticker)}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
setJobs([]);
|
||||||
|
} else {
|
||||||
|
const data = await res.json();
|
||||||
|
setJobs(data.jobs || []);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to fetch jobs:", e);
|
||||||
|
setJobs([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchJobs();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [ticker]);
|
||||||
|
|
||||||
|
const fetchDetails = async (jobId: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/jobs/${jobId}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
setSelected({ id: jobId, error: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
setSelected(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to fetch job details:", e);
|
||||||
|
setSelected({ id: jobId, error: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-800 mb-2">Job History</h3>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<button onClick={fetchJobs} className="text-sm text-blue-600 hover:underline">Refresh</button>
|
||||||
|
<span className="text-xs text-gray-500">{loading ? "Loading..." : `${jobs.length} jobs`}</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{jobs.length === 0 ? (
|
||||||
|
<p className="text-gray-500">No recent jobs for {ticker}</p>
|
||||||
|
) : (
|
||||||
|
jobs.map((j: any) => (
|
||||||
|
<div key={j.id} className="p-3 border border-gray-200 rounded bg-gray-50">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="font-medium">Job: <span className="text-blue-600">{j.id}</span></div>
|
||||||
|
<div className="text-xs text-gray-600">State: <strong>{j.state}</strong></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<a href={`/api/jobs/${j.id}`} target="_blank" rel="noreferrer" className="text-sm text-blue-600 hover:underline">API</a>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (selected?.id === j.id) {
|
||||||
|
setSelected(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchDetails(j.id);
|
||||||
|
}}
|
||||||
|
className="text-sm text-gray-700 underline"
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selected?.id === j.id && (
|
||||||
|
<pre className="mt-2 text-xs bg-white p-2 rounded overflow-x-auto">{JSON.stringify(selected, null, 2)}</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
|
|||||||
import { useLoaderData, useNavigate, useLocation } from "react-router";
|
import { useLoaderData, useNavigate, useLocation } from "react-router";
|
||||||
import TradingViewChart from "../components/TradingViewChart";
|
import TradingViewChart from "../components/TradingViewChart";
|
||||||
import Navbar from "../components/Navbar";
|
import Navbar from "../components/Navbar";
|
||||||
|
import JobHistory from "../components/JobHistory";
|
||||||
import type { TradingDecision, AnalystReport, DebateRound } from "../types/agents";
|
import type { TradingDecision, AnalystReport, DebateRound } from "../types/agents";
|
||||||
|
|
||||||
export const meta = () => [{ title: "Stock Detail - AITrader" }];
|
export const meta = () => [{ title: "Stock Detail - AITrader" }];
|
||||||
@@ -295,6 +296,9 @@ export default function StockDetail() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Job history */}
|
||||||
|
<JobHistory ticker={ticker} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 bg-white rounded-xl shadow-lg p-6 border border-gray-200">
|
<div className="mt-6 bg-white rounded-xl shadow-lg p-6 border border-gray-200">
|
||||||
|
|||||||
Reference in New Issue
Block a user