refactor frontend api client

This commit is contained in:
0nikod
2026-05-14 08:30:49 +08:00
parent 75d84b84b8
commit 75c17597eb
10 changed files with 242 additions and 178 deletions
+2 -6
View File
@@ -7,6 +7,7 @@ import { CategoryService } from './utils/categoryService';
import { settings } from './utils/settings';
import { metaService } from './utils/metaService';
import { textToTags, applyAutoMeta } from './utils/promptParser';
import { simplePromptApi } from './api/client';
const props = defineProps<{
initialPrompt: string;
@@ -57,12 +58,7 @@ const saveChanges = async () => {
let ratingTagNames: string[] = [];
try {
const resp = await fetch('/simple-prompt/get-tags-details', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ names: tagNames, fast: true })
});
const categoriesMap = await resp.json();
const categoriesMap = await simplePromptApi.getTagsDetails(tagNames, true);
// Extract rating tag names (category 7)
ratingTagNames = tagNames.filter(name =>
+114
View File
@@ -0,0 +1,114 @@
import type {
AddTagsPayload,
ApiStatusResponse,
CategoryRecord,
PresetsResponse,
PresetRecord,
TagRecord,
TagsListResponse,
ToggleLikeResponse,
UpdateCheckResponse,
} from './types';
async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
const response = await fetch(url, init);
const data = await response.json().catch(() => ({}));
if (!response.ok) {
const message = typeof data?.error === 'string' ? data.error : response.statusText;
throw new Error(message || `HTTP error ${response.status}`);
}
return data as T;
}
function jsonRequest(method: string, body?: unknown): RequestInit {
return {
method,
headers: { 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
};
}
export const simplePromptApi = {
async searchTags(query: string, limit = 20, useAliases = false, categories: number[] = []): Promise<TagRecord[]> {
const params = new URLSearchParams({
q: query,
limit: limit.toString(),
use_aliases: useAliases.toString(),
categories: categories.join(','),
});
return requestJson<TagRecord[]>(`/simple-prompt/search-tags?${params.toString()}`);
},
async getTagsDetails(names: string[], fast = false): Promise<Record<string, number>> {
if (names.length === 0) return {};
return requestJson<Record<string, number>>(
'/simple-prompt/get-tags-details',
jsonRequest('POST', { names, fast }),
);
},
async listTags(source: string, limit: number, offset: number, query: string): Promise<TagsListResponse> {
const params = new URLSearchParams({
source,
limit: limit.toString(),
offset: offset.toString(),
q: query,
});
return requestJson<TagsListResponse>(`/simple-prompt/tags/list?${params.toString()}`);
},
async deleteTag(name: string, source: string): Promise<ApiStatusResponse> {
return requestJson<ApiStatusResponse>('/simple-prompt/tags/delete', jsonRequest('DELETE', { name, source }));
},
async addTag(payload: AddTagsPayload): Promise<ApiStatusResponse> {
return requestJson<ApiStatusResponse>('/simple-prompt/add-custom-tag', jsonRequest('POST', payload));
},
async addTags(tags: AddTagsPayload[]): Promise<ApiStatusResponse> {
return requestJson<ApiStatusResponse>('/simple-prompt/add-custom-tag', jsonRequest('POST', { tags }));
},
async toggleLikeTag(payload: {
name: string;
is_liked: boolean;
category?: number;
post_count?: number;
alias?: string[];
}): Promise<ToggleLikeResponse> {
return requestJson<ToggleLikeResponse>('/simple-prompt/toggle-like-tag', jsonRequest('POST', payload));
},
async listCategories(): Promise<CategoryRecord[]> {
return requestJson<CategoryRecord[]>('/simple-prompt/categories/list');
},
async saveCategories(categories: CategoryRecord[]): Promise<ApiStatusResponse> {
return requestJson<ApiStatusResponse>('/simple-prompt/categories/save', jsonRequest('POST', { categories }));
},
async listPresets(): Promise<PresetsResponse> {
return requestJson<PresetsResponse>('/simple-prompt/presets/list');
},
async savePresets(presets: PresetRecord[]): Promise<ApiStatusResponse> {
return requestJson<ApiStatusResponse>('/simple-prompt/presets/save', jsonRequest('POST', { presets }));
},
async checkUpdate(): Promise<UpdateCheckResponse> {
return requestJson<UpdateCheckResponse>('/simple-prompt/check-update');
},
async updateTags(): Promise<ApiStatusResponse> {
return requestJson<ApiStatusResponse>('/simple-prompt/update-tags', jsonRequest('POST', {}));
},
async updateData(action: 'update_liked' | 'update_user'): Promise<ApiStatusResponse> {
return requestJson<ApiStatusResponse>('/simple-prompt/update-data', jsonRequest('POST', { action }));
},
};
+55
View File
@@ -0,0 +1,55 @@
export interface TagRecord {
name: string;
category: number;
post_count: number;
alias: string[];
priority?: number;
matched_alias?: string;
}
export interface TagsListResponse {
data: TagRecord[];
total: number;
}
export interface ApiStatusResponse {
status: string;
message?: string;
}
export interface ToggleLikeResponse extends ApiStatusResponse {
is_liked: boolean;
}
export interface CategoryRecord {
id: number;
name: string;
color: string;
}
export interface PresetRecord {
id: string;
name: string;
tags: string[];
}
export interface PresetsResponse {
defaults: PresetRecord[];
customs: PresetRecord[];
}
export interface UpdateCheckResponse {
update_available: boolean;
local_sha256: string;
remote_sha256: string;
version: string;
name: string;
}
export interface AddTagsPayload {
name: string;
category: number;
post_count: number;
alias: string[];
source?: string;
}
+2 -11
View File
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
import { Icon } from '@iconify/vue';
import { categoryService } from '../utils/categoryService';
import { TagCategory } from '../utils/types';
import { simplePromptApi } from '../api/client';
const props = defineProps<{
visible: boolean;
@@ -86,17 +87,7 @@ const handleSave = async () => {
source: props.targetSource || 'user'
};
const response = await fetch('/simple-prompt/add-custom-tag', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || response.statusText);
}
await simplePromptApi.addTag(payload);
successMsg.value = props.editMode ? t('customTag.successEdited') || 'Tag updated!' : t('customTag.successAdded');
setTimeout(() => {
+8 -28
View File
@@ -6,6 +6,7 @@ import { settings } from '../utils/settings';
import TagManager from './TagManager.vue';
import { categoryService, type CategoryItem } from '../utils/categoryService';
import { metaService } from '../utils/metaService';
import { simplePromptApi } from '../api/client';
const props = defineProps({
visible: {
@@ -169,12 +170,7 @@ const handleDataUpdate = async (action: 'update_github' | 'update_liked' | 'upda
updateStatus.value = t('settings.items.checkingUpdate');
try {
const checkResponse = await fetch('/simple-prompt/check-update');
const checkResult = await checkResponse.json();
if (!checkResponse.ok) {
throw new Error(checkResult.error || checkResponse.statusText);
}
const checkResult = await simplePromptApi.checkUpdate();
latestVersion.value = checkResult.version;
isChecking.value = false;
@@ -186,19 +182,12 @@ const handleDataUpdate = async (action: 'update_github' | 'update_liked' | 'upda
// 2. Proceed to update if available
updateStatus.value = t('settings.items.updating');
const updateResult = await simplePromptApi.updateTags();
const updateResponse = await fetch('/simple-prompt/update-tags', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const updateResult = await updateResponse.json();
if (updateResponse.ok && updateResult.status === 'success') {
if (updateResult.status === 'success') {
updateStatus.value = t('settings.items.updateSuccess');
} else {
throw new Error(updateResult.error || updateResponse.statusText);
throw new Error(updateResult.message || 'Update failed');
}
} finally {
isChecking.value = false; // ensure check flag cleared
@@ -207,21 +196,12 @@ const handleDataUpdate = async (action: 'update_github' | 'update_liked' | 'upda
else {
// Local Data Update (Liked or User)
updateStatus.value = t('settings.items.updating');
const endpoint = '/simple-prompt/update-data';
const payload = { action }; // 'update_liked' or 'update_user'
const result = await simplePromptApi.updateData(action);
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
if (response.ok && result.status === 'success') {
if (result.status === 'success') {
updateStatus.value = result.message || t('settings.items.updateSuccess');
} else {
throw new Error(result.error || response.statusText);
throw new Error(result.message || 'Update failed');
}
}
+15 -48
View File
@@ -4,8 +4,9 @@ import { Icon } from '@iconify/vue';
import { useI18n } from 'vue-i18n';
import CustomTagModal from './CustomTagModal.vue';
import { settings } from '../utils/settings';
import { type TagCategory, CATEGORY_COLORS } from '../utils/types'; // Keep for now if used, but prefer service
import { categoryService } from '../utils/categoryService';
import { simplePromptApi } from '../api/client';
import type { TagRecord } from '../api/types';
const { t } = useI18n();
@@ -18,7 +19,7 @@ const tabs = computed(() => [
]);
// Data
const tags = ref<any[]>([]);
const tags = ref<TagRecord[]>([]);
const totalTags = ref(0);
const loading = ref(false);
const searchQuery = ref('');
@@ -27,29 +28,16 @@ const pageSize = 20;
// Edit Modal
const showEditModal = ref(false);
const editingTag = ref<any>(null);
const editingTag = ref<TagRecord | null>(null);
// Fetch Tags
const fetchTags = async () => {
loading.value = true;
try {
const offset = (currentPage.value - 1) * pageSize;
const params = new URLSearchParams({
source: activeTab.value,
limit: pageSize.toString(),
offset: offset.toString(),
q: searchQuery.value
});
const response = await fetch(`/simple-prompt/tags/list?${params.toString()}`);
const result = await response.json();
if (response.ok) {
tags.value = result.data || [];
totalTags.value = result.total || 0;
} else {
console.error('Fetch tags failed:', result.error);
}
const result = await simplePromptApi.listTags(activeTab.value, pageSize, offset, searchQuery.value);
tags.value = result.data || [];
totalTags.value = result.total || 0;
} catch (e) {
console.error('Fetch tags error:', e);
} finally {
@@ -58,28 +46,18 @@ const fetchTags = async () => {
};
// Actions
const handleDelete = async (tag: any) => {
const handleDelete = async (tag: TagRecord) => {
if (!confirm(t('common.confirm') + ` Delete '${tag.name}'?`)) return;
try {
const response = await fetch('/simple-prompt/tags/delete', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: tag.name, source: activeTab.value })
});
if (response.ok) {
fetchTags();
} else {
const res = await response.json();
alert('Delete failed: ' + res.error);
}
await simplePromptApi.deleteTag(tag.name, activeTab.value);
fetchTags();
} catch (e: any) {
alert('Delete failed: ' + e.message);
}
};
const handleEdit = (tag: any) => {
const handleEdit = (tag: TagRecord) => {
editingTag.value = tag;
showEditModal.value = true;
};
@@ -134,21 +112,10 @@ const handleInlineAdd = async () => {
source: activeTab.value
}));
const response = await fetch('/simple-prompt/add-custom-tag', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tags: tagsPayload })
});
const res = await response.json();
if (response.ok) {
newTagName.value = '';
fetchTags();
// Show toast or alert? Just clean input is standard.
} else {
alert('Add failed: ' + res.error);
}
await simplePromptApi.addTags(tagsPayload);
newTagName.value = '';
fetchTags();
// Show toast or alert? Just clean input is standard.
} catch (e: any) {
alert('Add failed: ' + e.message);
} finally {
+7 -9
View File
@@ -4,6 +4,8 @@ import { useI18n } from 'vue-i18n';
import { Icon } from '@iconify/vue';
import { DuckDBService } from '../utils/duckdbService';
import { CategoryService } from '../utils/categoryService';
import { simplePromptApi } from '../api/client';
import type { TagRecord } from '../api/types';
const props = defineProps<{
visible: boolean;
@@ -17,7 +19,7 @@ const emit = defineEmits<{
}>();
const searchQuery = ref('');
const searchResults = ref<any[]>([]);
const searchResults = ref<TagRecord[]>([]);
const loading = ref(false);
const selectedCategories = ref<number[]>([]);
@@ -89,7 +91,7 @@ const handleSearchInput = () => {
};
// Add tag to prompt
const addTag = (tag: any) => {
const addTag = (tag: TagRecord) => {
emit('add-tag', tag.name, tag.category);
};
@@ -158,11 +160,11 @@ const getHighlightParts = (text: string, query: string): Array<{ text: string; m
};
// --- Like Functionality ---
const isLiked = (tag: any) => {
const isLiked = (tag: TagRecord) => {
return tag.priority === 1; // 1 = Liked source
};
const toggleLike = async (tag: any, event: Event) => {
const toggleLike = async (tag: TagRecord, event: Event) => {
event.stopPropagation();
const currentlyLiked = isLiked(tag);
@@ -178,11 +180,7 @@ const toggleLike = async (tag: any, event: Event) => {
alias: tag.alias
};
await fetch('/simple-prompt/toggle-like-tag', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
await simplePromptApi.toggleLikeTag(payload);
// Success - no undo needed
// Re-search to sort correctly if needed?
+14 -26
View File
@@ -1,5 +1,6 @@
import { TagCategory, CATEGORY_COLORS } from './types';
import { reactive } from 'vue';
import { simplePromptApi } from '../api/client';
export interface CategoryData {
value: number;
@@ -51,21 +52,18 @@ export class CategoryService {
public async fetchCategories(): Promise<void> {
try {
const response = await fetch('/simple-prompt/categories/list');
if (response.ok) {
const data = await response.json();
const data = await simplePromptApi.listCategories();
if (Array.isArray(data)) {
// Clear custom ones if any, but keep defaults if fetch fails next time?
// Better to just update the map.
data.forEach((cat: any) => {
this._categories.set(cat.id, {
value: cat.id,
label: cat.name,
color: cat.color
});
if (Array.isArray(data)) {
// Clear custom ones if any, but keep defaults if fetch fails next time?
// Better to just update the map.
data.forEach((cat) => {
this._categories.set(cat.id, {
value: cat.id,
label: cat.name,
color: cat.color
});
}
});
}
} catch (error) {
console.error("[CategoryService] Failed to fetch categories:", error);
@@ -76,19 +74,9 @@ export class CategoryService {
public async saveCustomCategories(categories: CategoryItem[]): Promise<void> {
try {
const response = await fetch('/simple-prompt/categories/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ categories })
});
if (response.ok) {
// Refresh local state after save
await this.fetchCategories();
} else {
console.error("[CategoryService] Failed to save categories:", await response.text());
throw new Error("Failed to save categories");
}
await simplePromptApi.saveCategories(categories);
// Refresh local state after save
await this.fetchCategories();
} catch (error) {
console.error("[CategoryService] Error saving categories:", error);
throw error;
+8 -27
View File
@@ -1,3 +1,6 @@
import { simplePromptApi } from '../api/client';
import type { TagRecord } from '../api/types';
export class DuckDBService {
private static instance: DuckDBService;
private isInitialized = false;
@@ -20,24 +23,13 @@ export class DuckDBService {
public async searchTags(query: string, limit: number = 20, useAliases: boolean = false, categories: number[] = []): Promise<any[]> {
const params = new URLSearchParams({
q: query,
limit: limit.toString(),
use_aliases: useAliases.toString(),
categories: categories.join(',')
});
public async searchTags(query: string, limit: number = 20, useAliases: boolean = false, categories: number[] = []): Promise<TagRecord[]> {
try {
const response = await fetch(`/simple-prompt/search-tags?${params.toString()}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const results = await response.json();
const results = await simplePromptApi.searchTags(query, limit, useAliases, categories);
// Handle the same alias matching logic as before for UI highlighting
if (useAliases && results.length > 0) {
return results.map((row: any) => {
return results.map((row) => {
const nameMatches = row.name && row.name.toLowerCase().includes(query.toLowerCase());
// If name matches, it's a direct match
@@ -68,22 +60,11 @@ export class DuckDBService {
}
}
public async getTagsDetails(names: string[]): Promise<Record<string, number>> {
public async getTagsDetails(names: string[], fast = false): Promise<Record<string, number>> {
if (names.length === 0) return {};
try {
const response = await fetch('/simple-prompt/get-tags-details', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ names })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
return await simplePromptApi.getTagsDetails(names, fast);
} catch (error) {
console.error("[DuckDB] Get tags details failed:", error);
return {};
+17 -23
View File
@@ -1,11 +1,13 @@
import { ref } from 'vue';
import { simplePromptApi } from '../api/client';
import type { PresetRecord } from '../api/types';
class MetaService {
private static instance: MetaService;
// Preset support
public presets = ref<any[]>([]); // { id, name, tags }
public defaultPresets = ref<any[]>([]);
public customPresets = ref<any[]>([]);
public presets = ref<PresetRecord[]>([]);
public defaultPresets = ref<PresetRecord[]>([]);
public customPresets = ref<PresetRecord[]>([]);
public activePresetId = ref<string>('');
public loading = ref(false);
@@ -24,35 +26,27 @@ class MetaService {
public async fetchPresets() {
try {
const response = await fetch('/simple-prompt/presets/list');
if (response.ok) {
const data = await response.json();
this.defaultPresets.value = data.defaults || [];
this.customPresets.value = data.customs || [];
const data = await simplePromptApi.listPresets();
this.defaultPresets.value = data.defaults || [];
this.customPresets.value = data.customs || [];
// Merge
this.presets.value = [...this.defaultPresets.value, ...this.customPresets.value];
// Merge
this.presets.value = [...this.defaultPresets.value, ...this.customPresets.value];
// Restore active preset logic or default to first if none
if (!this.activePresetId.value && this.presets.value.length > 0) {
// Check storage?
// For now default to first
this.activePresetId.value = this.presets.value[0].id;
}
// Restore active preset logic or default to first if none
if (!this.activePresetId.value && this.presets.value.length > 0) {
// Check storage?
// For now default to first
this.activePresetId.value = this.presets.value[0].id;
}
} catch (e) {
console.error("Error fetching presets:", e);
}
}
public async saveCustomPresets(customs: any[]) {
public async saveCustomPresets(customs: PresetRecord[]) {
try {
const response = await fetch('/simple-prompt/presets/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ presets: customs })
});
if (!response.ok) throw new Error("Failed");
await simplePromptApi.savePresets(customs);
await this.fetchPresets();
} catch (e) {
console.error("Error saving presets:", e);