Merge branch 't3code/expand-research-management-plan'
# Conflicts: # app/analysis/page.tsx # app/watchlist/page.tsx # components/shell/app-shell.tsx # lib/api.ts # lib/query/options.ts # lib/server/api/app.ts # lib/server/db/index.test.ts # lib/server/db/index.ts # lib/server/db/schema.ts # lib/server/repos/research-journal.ts # lib/types.ts
This commit is contained in:
215
lib/api.ts
215
lib/api.ts
@@ -12,11 +12,22 @@ import type {
|
||||
Holding,
|
||||
PortfolioInsight,
|
||||
PortfolioSummary,
|
||||
ResearchArtifact,
|
||||
ResearchArtifactKind,
|
||||
ResearchArtifactSource,
|
||||
ResearchJournalEntry,
|
||||
ResearchJournalEntryType,
|
||||
SearchAnswerResponse,
|
||||
SearchResult,
|
||||
SearchSource,
|
||||
ResearchLibraryResponse,
|
||||
ResearchMemo,
|
||||
ResearchMemoConviction,
|
||||
ResearchMemoEvidenceLink,
|
||||
ResearchMemoRating,
|
||||
ResearchMemoSection,
|
||||
ResearchPacket,
|
||||
ResearchWorkspace,
|
||||
Task,
|
||||
TaskStatus,
|
||||
TaskTimeline,
|
||||
@@ -108,7 +119,7 @@ async function unwrapData<T>(result: TreatyResult, fallback: string) {
|
||||
|
||||
async function requestJson<T>(input: {
|
||||
path: string;
|
||||
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
||||
method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
||||
body?: unknown;
|
||||
}, fallback: string) {
|
||||
const response = await fetch(`${API_BASE}${input.path}`, {
|
||||
@@ -209,6 +220,208 @@ export async function createResearchJournalEntry(input: {
|
||||
}, 'Unable to create journal entry');
|
||||
}
|
||||
|
||||
export async function getResearchWorkspace(ticker: string) {
|
||||
return await requestJson<{ workspace: ResearchWorkspace }>({
|
||||
path: `/api/research/workspace?ticker=${encodeURIComponent(ticker.trim().toUpperCase())}`
|
||||
}, 'Unable to fetch research workspace');
|
||||
}
|
||||
|
||||
export async function listResearchLibrary(input: {
|
||||
ticker: string;
|
||||
q?: string;
|
||||
kind?: ResearchArtifactKind;
|
||||
tag?: string;
|
||||
source?: ResearchArtifactSource;
|
||||
linkedToMemo?: boolean;
|
||||
limit?: number;
|
||||
}) {
|
||||
const params = new URLSearchParams({
|
||||
ticker: input.ticker.trim().toUpperCase()
|
||||
});
|
||||
|
||||
if (input.q?.trim()) {
|
||||
params.set('q', input.q.trim());
|
||||
}
|
||||
|
||||
if (input.kind) {
|
||||
params.set('kind', input.kind);
|
||||
}
|
||||
|
||||
if (input.tag?.trim()) {
|
||||
params.set('tag', input.tag.trim());
|
||||
}
|
||||
|
||||
if (input.source) {
|
||||
params.set('source', input.source);
|
||||
}
|
||||
|
||||
if (input.linkedToMemo !== undefined) {
|
||||
params.set('linkedToMemo', input.linkedToMemo ? 'true' : 'false');
|
||||
}
|
||||
|
||||
if (typeof input.limit === 'number') {
|
||||
params.set('limit', String(input.limit));
|
||||
}
|
||||
|
||||
return await requestJson<ResearchLibraryResponse>({
|
||||
path: `/api/research/library?${params.toString()}`
|
||||
}, 'Unable to fetch research library');
|
||||
}
|
||||
|
||||
export async function createResearchArtifact(input: {
|
||||
ticker: string;
|
||||
accessionNumber?: string;
|
||||
kind: ResearchArtifactKind;
|
||||
source?: ResearchArtifactSource;
|
||||
subtype?: string;
|
||||
title?: string;
|
||||
summary?: string;
|
||||
bodyMarkdown?: string;
|
||||
tags?: string[];
|
||||
metadata?: Record<string, unknown>;
|
||||
}) {
|
||||
return await requestJson<{ artifact: ResearchArtifact }>({
|
||||
path: '/api/research/library',
|
||||
method: 'POST',
|
||||
body: {
|
||||
...input,
|
||||
ticker: input.ticker.trim().toUpperCase()
|
||||
}
|
||||
}, 'Unable to create research artifact');
|
||||
}
|
||||
|
||||
export async function updateResearchArtifact(id: number, input: {
|
||||
title?: string;
|
||||
summary?: string;
|
||||
bodyMarkdown?: string;
|
||||
tags?: string[];
|
||||
metadata?: Record<string, unknown>;
|
||||
}) {
|
||||
return await requestJson<{ artifact: ResearchArtifact }>({
|
||||
path: `/api/research/library/${id}`,
|
||||
method: 'PATCH',
|
||||
body: input
|
||||
}, 'Unable to update research artifact');
|
||||
}
|
||||
|
||||
export async function deleteResearchArtifact(id: number) {
|
||||
return await requestJson<{ success: boolean }>({
|
||||
path: `/api/research/library/${id}`,
|
||||
method: 'DELETE'
|
||||
}, 'Unable to delete research artifact');
|
||||
}
|
||||
|
||||
export async function uploadResearchArtifact(input: {
|
||||
ticker: string;
|
||||
file: File;
|
||||
title?: string;
|
||||
summary?: string;
|
||||
tags?: string[];
|
||||
}) {
|
||||
const form = new FormData();
|
||||
form.set('ticker', input.ticker.trim().toUpperCase());
|
||||
form.set('file', input.file);
|
||||
|
||||
if (input.title?.trim()) {
|
||||
form.set('title', input.title.trim());
|
||||
}
|
||||
|
||||
if (input.summary?.trim()) {
|
||||
form.set('summary', input.summary.trim());
|
||||
}
|
||||
|
||||
if (input.tags && input.tags.length > 0) {
|
||||
form.set('tags', input.tags.join(','));
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}/api/research/library/upload`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
cache: 'no-store',
|
||||
body: form
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new ApiError(
|
||||
extractErrorMessage({ value: payload }, 'Unable to upload research file'),
|
||||
response.status
|
||||
);
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
throw new ApiError('Unable to upload research file', response.status);
|
||||
}
|
||||
|
||||
return payload as { artifact: ResearchArtifact };
|
||||
}
|
||||
|
||||
export function getResearchArtifactFileUrl(id: number) {
|
||||
return `${API_BASE}/api/research/library/${id}/file`;
|
||||
}
|
||||
|
||||
export async function getResearchMemo(ticker: string) {
|
||||
return await requestJson<{ memo: ResearchMemo | null }>({
|
||||
path: `/api/research/memo?ticker=${encodeURIComponent(ticker.trim().toUpperCase())}`
|
||||
}, 'Unable to fetch research memo');
|
||||
}
|
||||
|
||||
export async function upsertResearchMemo(input: {
|
||||
ticker: string;
|
||||
rating?: ResearchMemoRating | null;
|
||||
conviction?: ResearchMemoConviction | null;
|
||||
timeHorizonMonths?: number | null;
|
||||
packetTitle?: string;
|
||||
packetSubtitle?: string;
|
||||
thesisMarkdown?: string;
|
||||
variantViewMarkdown?: string;
|
||||
catalystsMarkdown?: string;
|
||||
risksMarkdown?: string;
|
||||
disconfirmingEvidenceMarkdown?: string;
|
||||
nextActionsMarkdown?: string;
|
||||
}) {
|
||||
return await requestJson<{ memo: ResearchMemo }>({
|
||||
path: '/api/research/memo',
|
||||
method: 'PUT',
|
||||
body: {
|
||||
...input,
|
||||
ticker: input.ticker.trim().toUpperCase()
|
||||
}
|
||||
}, 'Unable to save research memo');
|
||||
}
|
||||
|
||||
export async function addResearchMemoEvidence(input: {
|
||||
memoId: number;
|
||||
artifactId: number;
|
||||
section: ResearchMemoSection;
|
||||
annotation?: string;
|
||||
sortOrder?: number;
|
||||
}) {
|
||||
return await requestJson<{ evidence: ResearchMemoEvidenceLink[] }>({
|
||||
path: `/api/research/memo/${input.memoId}/evidence`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
artifactId: input.artifactId,
|
||||
section: input.section,
|
||||
annotation: input.annotation,
|
||||
sortOrder: input.sortOrder
|
||||
}
|
||||
}, 'Unable to attach memo evidence');
|
||||
}
|
||||
|
||||
export async function deleteResearchMemoEvidence(memoId: number, linkId: number) {
|
||||
return await requestJson<{ success: boolean }>({
|
||||
path: `/api/research/memo/${memoId}/evidence/${linkId}`,
|
||||
method: 'DELETE'
|
||||
}, 'Unable to delete memo evidence');
|
||||
}
|
||||
|
||||
export async function getResearchPacket(ticker: string) {
|
||||
return await requestJson<{ packet: ResearchPacket }>({
|
||||
path: `/api/research/packet?ticker=${encodeURIComponent(ticker.trim().toUpperCase())}`
|
||||
}, 'Unable to fetch research packet');
|
||||
}
|
||||
|
||||
export async function updateResearchJournalEntry(id: number, input: {
|
||||
title?: string;
|
||||
bodyMarkdown?: string;
|
||||
|
||||
Reference in New Issue
Block a user