|
|
|
|
|
|
|
|
|
|
|
|
|
|
import * as d3 from "d3"; |
|
|
import URLHandler from "../utils/URLHandler"; |
|
|
import {cleanSpecials} from "../utils/Util"; |
|
|
import {AnalyzeResponse, AnalyzeResult, TokenWithOffset} from "./generatedSchemas"; |
|
|
|
|
|
export type FrontendToken = TokenWithOffset & { bpe_merged?: boolean }; |
|
|
export interface FrontendAnalyzeResult extends AnalyzeResult { |
|
|
bpe_strings: FrontendToken[]; |
|
|
originalTokens: FrontendToken[]; |
|
|
mergedTokens: FrontendToken[]; |
|
|
originalToMergedMap: number[]; |
|
|
originalText: string; |
|
|
} |
|
|
|
|
|
|
|
|
export type AnalyzedText = FrontendAnalyzeResult; |
|
|
|
|
|
|
|
|
export type AnalysisData = AnalyzeResponse; |
|
|
export type { AnalyzeResponse, TokenWithOffset }; |
|
|
|
|
|
export class TextAnalysisAPI { |
|
|
private adminToken: string | null = null; |
|
|
|
|
|
constructor(private baseURL: string = null) { |
|
|
if (this.baseURL == null) { |
|
|
this.baseURL = URLHandler.basicURL(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public setAdminToken(token: string | null): void { |
|
|
this.adminToken = token; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private getHeaders(additionalHeaders?: Record<string, string>): Record<string, string> { |
|
|
const headers: Record<string, string> = { |
|
|
"Content-type": "application/json; charset=UTF-8", |
|
|
...additionalHeaders |
|
|
}; |
|
|
|
|
|
|
|
|
if (this.adminToken) { |
|
|
headers['X-Admin-Token'] = this.adminToken; |
|
|
} |
|
|
|
|
|
return headers; |
|
|
} |
|
|
|
|
|
|
|
|
public list_demos(path?: string): Promise<{ path: string, items: Array<{type: 'folder'|'file', name: string, path: string}> }> { |
|
|
const url = this.baseURL + '/api/list_demos' + (path ? `?path=${encodeURIComponent(path)}` : ''); |
|
|
return d3.json(url); |
|
|
} |
|
|
|
|
|
public save_demo(name: string, data: AnalyzeResponse, path: string = '/', overwrite: boolean = false): Promise<{ success: boolean, exists?: boolean, message?: string, file?: string }> { |
|
|
return d3.json(this.baseURL + '/api/save_demo', { |
|
|
method: "POST", |
|
|
body: JSON.stringify({ name, data, path, overwrite }), |
|
|
headers: this.getHeaders() |
|
|
}); |
|
|
} |
|
|
|
|
|
public delete_demo(file: string): Promise<{ success: boolean, message?: string }> { |
|
|
return d3.json(this.baseURL + '/api/delete_demo', { |
|
|
method: "POST", |
|
|
body: JSON.stringify({ file }), |
|
|
headers: this.getHeaders() |
|
|
}); |
|
|
} |
|
|
|
|
|
public move_demo(file: string, targetPath: string): Promise<{ success: boolean, message?: string }> { |
|
|
return d3.json(this.baseURL + '/api/move_demo', { |
|
|
method: "POST", |
|
|
body: JSON.stringify({ file, target_path: targetPath }), |
|
|
headers: this.getHeaders() |
|
|
}); |
|
|
} |
|
|
|
|
|
public move_folder(path: string, targetPath: string): Promise<{ success: boolean, message?: string }> { |
|
|
return d3.json(this.baseURL + '/api/move_demo', { |
|
|
method: "POST", |
|
|
body: JSON.stringify({ path, target_path: targetPath }), |
|
|
headers: this.getHeaders() |
|
|
}); |
|
|
} |
|
|
|
|
|
public rename_demo(file: string, newName: string): Promise<{ success: boolean, message?: string }> { |
|
|
return d3.json(this.baseURL + '/api/rename_demo', { |
|
|
method: "POST", |
|
|
body: JSON.stringify({ file, new_name: newName }), |
|
|
headers: this.getHeaders() |
|
|
}); |
|
|
} |
|
|
|
|
|
public rename_folder(path: string, newName: string): Promise<{ success: boolean, message?: string }> { |
|
|
return d3.json(this.baseURL + '/api/rename_folder', { |
|
|
method: "POST", |
|
|
body: JSON.stringify({ path, new_name: newName }), |
|
|
headers: this.getHeaders() |
|
|
}); |
|
|
} |
|
|
|
|
|
public delete_folder(path: string): Promise<{ success: boolean, message?: string }> { |
|
|
return d3.json(this.baseURL + '/api/delete_folder', { |
|
|
method: "POST", |
|
|
body: JSON.stringify({ path }), |
|
|
headers: this.getHeaders() |
|
|
}); |
|
|
} |
|
|
|
|
|
public list_all_folders(): Promise<{ folders: string[] }> { |
|
|
return d3.json(this.baseURL + '/api/list_all_folders'); |
|
|
} |
|
|
|
|
|
public create_folder(parentPath: string, folderName: string): Promise<{ success: boolean, message?: string }> { |
|
|
return d3.json(this.baseURL + '/api/create_folder', { |
|
|
method: "POST", |
|
|
body: JSON.stringify({ parent_path: parentPath, folder_name: folderName }), |
|
|
headers: this.getHeaders() |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private buildAnalyzePayload( |
|
|
model: string, |
|
|
text: string, |
|
|
bitmask: number[] = null, |
|
|
stream: boolean = false |
|
|
): any { |
|
|
const payload: any = { |
|
|
model, |
|
|
text: cleanSpecials(text) |
|
|
}; |
|
|
if (bitmask) { |
|
|
payload['bitmask'] = bitmask; |
|
|
} |
|
|
if (stream) { |
|
|
payload['stream'] = true; |
|
|
} |
|
|
return payload; |
|
|
} |
|
|
|
|
|
public analyze( |
|
|
model: string, |
|
|
text: string, |
|
|
bitmask: number[] = null, |
|
|
stream: boolean = false, |
|
|
onProgress?: (step: number, totalSteps: number, stage: string, percentage?: number) => void |
|
|
): Promise<AnalyzeResponse> { |
|
|
|
|
|
if (stream) { |
|
|
return this.analyzeWithProgress(model, text, onProgress); |
|
|
} |
|
|
|
|
|
|
|
|
const payload = this.buildAnalyzePayload(model, text, bitmask, stream); |
|
|
return d3.json(this.baseURL + '/api/analyze', { |
|
|
method: "POST", |
|
|
body: JSON.stringify(payload), |
|
|
headers: { |
|
|
"Content-type": "application/json; charset=UTF-8" |
|
|
} |
|
|
}).then((response: any) => { |
|
|
|
|
|
if (response && response.success === false) { |
|
|
throw new Error(response.message || '分析失败'); |
|
|
} |
|
|
return response as AnalyzeResponse; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public fetchUrlText(url: string): Promise<{success: boolean, text?: string, url?: string, char_count?: number, message?: string}> { |
|
|
return d3.json(this.baseURL + '/api/fetch_url', { |
|
|
method: "POST", |
|
|
body: JSON.stringify({ url }), |
|
|
headers: { |
|
|
"Content-type": "application/json; charset=UTF-8" |
|
|
} |
|
|
}).then((response: any) => { |
|
|
|
|
|
if (response && response.success === false) { |
|
|
throw new Error(response.message || 'URL 文本提取失败'); |
|
|
} |
|
|
return response; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public getAvailableModels(): Promise<{ success: boolean, models: string[] }> { |
|
|
return d3.json(this.baseURL + '/api/available_models'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public getCurrentModel(): Promise<{ |
|
|
success: boolean, |
|
|
model: string, |
|
|
loading: boolean, |
|
|
device_type: 'cpu' | 'cuda' | 'mps', |
|
|
use_int8: boolean, |
|
|
use_bfloat16: boolean |
|
|
}> { |
|
|
return d3.json(this.baseURL + '/api/current_model'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public switchModel( |
|
|
model: string, |
|
|
use_int8?: boolean, |
|
|
use_bfloat16?: boolean |
|
|
): Promise<{ success: boolean, message?: string, model?: string }> { |
|
|
return d3.json(this.baseURL + '/api/switch_model', { |
|
|
method: "POST", |
|
|
body: JSON.stringify({ |
|
|
model, |
|
|
use_int8: use_int8 || false, |
|
|
use_bfloat16: use_bfloat16 || false |
|
|
}), |
|
|
headers: this.getHeaders() |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private analyzeWithProgress( |
|
|
model: string, |
|
|
text: string, |
|
|
onProgress?: (step: number, totalSteps: number, stage: string, percentage?: number) => void |
|
|
): Promise<AnalyzeResponse> { |
|
|
return new Promise((resolve, reject) => { |
|
|
const payload = this.buildAnalyzePayload(model, text, null, true); |
|
|
|
|
|
|
|
|
fetch(this.baseURL + '/api/analyze', { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json; charset=UTF-8' |
|
|
}, |
|
|
body: JSON.stringify(payload) |
|
|
}).then(response => { |
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
|
} |
|
|
|
|
|
const reader = response.body.getReader(); |
|
|
const decoder = new TextDecoder(); |
|
|
let buffer = ''; |
|
|
|
|
|
const readChunk = (): Promise<void> => { |
|
|
return reader.read().then(({ done, value }) => { |
|
|
if (done) { |
|
|
if (buffer.trim()) { |
|
|
|
|
|
this.processSSEMessage(buffer, onProgress, resolve, reject); |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
buffer += decoder.decode(value, { stream: true }); |
|
|
const lines = buffer.split('\n'); |
|
|
buffer = lines.pop() || ''; |
|
|
|
|
|
for (const line of lines) { |
|
|
if (line.startsWith('data: ')) { |
|
|
const data = line.slice(6); |
|
|
this.processSSEMessage(data, onProgress, resolve, reject); |
|
|
} |
|
|
} |
|
|
|
|
|
return readChunk(); |
|
|
}); |
|
|
}; |
|
|
|
|
|
return readChunk(); |
|
|
}).catch(error => { |
|
|
reject(error); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private processSSEMessage( |
|
|
data: string, |
|
|
onProgress: (step: number, totalSteps: number, stage: string, percentage?: number) => void, |
|
|
resolve: (value: AnalyzeResponse) => void, |
|
|
reject: (reason?: any) => void |
|
|
): void { |
|
|
try { |
|
|
const parsed = JSON.parse(data); |
|
|
|
|
|
if (parsed.type === 'progress') { |
|
|
|
|
|
if (onProgress) { |
|
|
onProgress(parsed.step, parsed.total_steps, parsed.stage, parsed.percentage); |
|
|
} |
|
|
} else if (parsed.type === 'result') { |
|
|
|
|
|
const resultData = parsed.data; |
|
|
if (resultData && resultData.success === false) { |
|
|
reject(new Error(resultData.message || '分析失败')); |
|
|
} else { |
|
|
resolve(resultData as AnalyzeResponse); |
|
|
} |
|
|
} else if (parsed.type === 'error') { |
|
|
|
|
|
reject(new Error(parsed.message || '分析失败')); |
|
|
} |
|
|
} catch (e) { |
|
|
|
|
|
console.warn('Failed to parse SSE message:', e, data); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|