chore: checkpoint ai search comments and i18n foundation

This commit is contained in:
2026-03-28 17:17:31 +08:00
parent d18a709987
commit ec96d91548
71 changed files with 9494 additions and 423 deletions

View File

@@ -32,6 +32,10 @@ export interface Comment {
avatar: string | null;
content: string | null;
reply_to: string | null;
reply_to_comment_id: number | null;
scope: 'article' | 'paragraph';
paragraph_key: string | null;
paragraph_excerpt: string | null;
approved: boolean | null;
created_at: string;
updated_at: string;
@@ -42,7 +46,16 @@ export interface CreateCommentInput {
nickname: string;
email?: string;
content: string;
scope?: 'article' | 'paragraph';
paragraphKey?: string;
paragraphExcerpt?: string;
replyTo?: string | null;
replyToCommentId?: number | null;
}
export interface ParagraphCommentSummary {
paragraph_key: string;
count: number;
}
export interface ApiTag {
@@ -98,6 +111,23 @@ export interface ApiSiteSettings {
social_email: string | null;
location: string | null;
tech_stack: string[] | null;
ai_enabled: boolean;
}
export interface AiSource {
slug: string;
title: string;
excerpt: string;
score: number;
chunk_index: number;
}
export interface AiAskResponse {
question: string;
answer: string;
sources: AiSource[];
indexed_chunks: number;
last_indexed_at: string | null;
}
export interface ApiSearchResult {
@@ -153,6 +183,9 @@ export const DEFAULT_SITE_SETTINGS: SiteSettings = {
email: 'mailto:hello@termi.dev',
},
techStack: ['Astro', 'Svelte', 'Tailwind CSS', 'TypeScript'],
ai: {
enabled: false,
},
};
const formatPostDate = (dateString: string) => dateString.slice(0, 10);
@@ -244,6 +277,9 @@ const normalizeSiteSettings = (settings: ApiSiteSettings): SiteSettings => ({
email: settings.social_email || DEFAULT_SITE_SETTINGS.social.email,
},
techStack: settings.tech_stack?.length ? settings.tech_stack : DEFAULT_SITE_SETTINGS.techStack,
ai: {
enabled: Boolean(settings.ai_enabled),
},
});
class ApiClient {
@@ -293,14 +329,32 @@ class ApiClient {
return posts.find(post => post.slug === slug) || null;
}
async getComments(postSlug: string, options?: { approved?: boolean }): Promise<Comment[]> {
async getComments(
postSlug: string,
options?: {
approved?: boolean;
scope?: 'article' | 'paragraph';
paragraphKey?: string;
}
): Promise<Comment[]> {
const params = new URLSearchParams({ post_slug: postSlug });
if (options?.approved !== undefined) {
params.set('approved', String(options.approved));
}
if (options?.scope) {
params.set('scope', options.scope);
}
if (options?.paragraphKey) {
params.set('paragraph_key', options.paragraphKey);
}
return this.fetch<Comment[]>(`/comments?${params.toString()}`);
}
async getParagraphCommentSummary(postSlug: string): Promise<ParagraphCommentSummary[]> {
const params = new URLSearchParams({ post_slug: postSlug });
return this.fetch<ParagraphCommentSummary[]>(`/comments/paragraphs/summary?${params.toString()}`);
}
async createComment(comment: CreateCommentInput): Promise<Comment> {
return this.fetch<Comment>('/comments', {
method: 'POST',
@@ -309,7 +363,11 @@ class ApiClient {
nickname: comment.nickname,
email: comment.email,
content: comment.content,
scope: comment.scope,
paragraphKey: comment.paragraphKey,
paragraphExcerpt: comment.paragraphExcerpt,
replyTo: comment.replyTo,
replyToCommentId: comment.replyToCommentId,
}),
});
}
@@ -398,6 +456,13 @@ class ApiClient {
})
);
}
async askAi(question: string): Promise<AiAskResponse> {
return this.fetch<AiAskResponse>('/ai/ask', {
method: 'POST',
body: JSON.stringify({ question }),
});
}
}
export const api = new ApiClient(API_BASE_URL);