mirror of
https://github.com/Buriburizaem0n/ai-dict.git
synced 2025-12-13 03:44:41 +00:00
Initial commit
This commit is contained in:
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
75
README.md
Normal file
75
README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# AI 词典 (AI Dictionary)
|
||||
|
||||
这是一个基于 Go 语言和大型语言模型 (LLM) API 构建的高性能、专业级实时在线词典。项目旨在提供快速、准确且内容丰富的单词查询体验,其核心能力完全由最先进的 AI 模型驱动。
|
||||
|
||||

|
||||
|
||||
## ✨ 功能特性 (Features)
|
||||
|
||||
* **实时 AI 生成**:所有词典数据均由 LLM 实时生成,内容鲜活且丰富。
|
||||
* **专业级内容**:可提供包含音标、多词性、中英释义及情景例句的专业级词典内容。
|
||||
* **极致性能优化**:
|
||||
* **毫秒级缓存**:内置内存缓存机制,重复查询的单词可实现瞬时响应。
|
||||
* **低 Token 消耗**:通过极限压缩的 Prompt Engineering,将单次查询的总 Token 消耗稳定在 200 以内,兼顾了速度与成本。
|
||||
* **智能AI指令**:通过为 AI 注入“编辑判断力”,使其能够返回现代、实用的释义,并自动省略古老或罕见的用法,确保内容的专业性。
|
||||
* **健壮的输入验证**:
|
||||
* 在前端和后端实施双重验证,限制输入内容的字符长度。
|
||||
* 有效防止长句或无效输入造成的资源浪费,并引导用户正确使用。
|
||||
* **优雅的错误处理**:前端代码具备防御性编程能力,即使 AI 返回非预期格式,页面也不会崩溃。
|
||||
|
||||
## 🛠️ 技术栈 (Tech Stack)
|
||||
|
||||
* **后端 (Backend)**: Go (Golang) `net/http` 标准库
|
||||
* **前端 (Frontend)**: HTML, CSS, Vanilla JavaScript
|
||||
* **AI 服务**: 可通过 [OpenRouter](https://openrouter.ai/) 等平台调用任意大语言模型 API
|
||||
|
||||
## 🚀 快速开始 (Getting Started)
|
||||
|
||||
请按照以下步骤在您的本地计算机上运行本项目。
|
||||
|
||||
### 1. 先决条件 (Prerequisites)
|
||||
|
||||
确保您的系统已经安装了 [Go 语言](https://go.dev/doc/install) (建议版本 1.18 或以上)。
|
||||
|
||||
### 2. 安装与配置 (Installation & Configuration)
|
||||
|
||||
1. **获取代码**:
|
||||
将项目文件(`main.go`, `static/` 文件夹等)放置在您选择的目录中。项目结构如下:
|
||||
```
|
||||
/ai-dictionary
|
||||
├── static/
|
||||
│ ├── index.html
|
||||
│ └── app.js
|
||||
└── main.go
|
||||
```
|
||||
|
||||
2. **获取并设置 API 密钥 (最关键的一步)**:
|
||||
* 访问 [OpenRouter](https://openrouter.ai/keys) 或其他您选择的 AI 服务商,获取您的 API Key。
|
||||
* 在main.go中配置url,Api-key以及选用的模型。
|
||||
* **提示**: 以上设置仅在当前终端会话中有效。要使其永久生效,请将命令添加到您的 shell 配置文件中 (如 `.zshrc` 或 `.bash_profile`)。
|
||||
|
||||
### 3. 运行项目 (Running the Application)
|
||||
|
||||
1. 打开终端,使用 `cd` 命令进入项目根目录 (`/ai-dictionary`)。
|
||||
2. 运行以下命令来启动 Go 服务器:
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
3. 如果一切顺利,您会看到提示 `Server starting on http://localhost:8080`。
|
||||
4. 打开您的浏览器,访问 [http://localhost:8080](http://localhost:8080) 即可开始使用!
|
||||
|
||||
## ⚙️ 核心设计 (Core Design)
|
||||
|
||||
本项目成功的关键在于**精准的 Prompt Engineering**和**极致的性能优化**。
|
||||
|
||||
* **Prompt 设计**: 我们通过多次迭代,将最初近 300 token 的复杂指令,压缩为约 60 token 的高效指令。通过赋予 AI “词典编辑”的角色,并使用明确的排除指令 (`OMIT archaic...`),我们引导模型在保持简洁的同时,输出高度专业和准确的内容。
|
||||
|
||||
* **性能优化**: 通过引入内存缓存,避免了对常用词的重复 API 调用,将响应时间从秒级降低到毫秒级。同时,前端和后端的双重输入验证有效保护了后端的 AI API 不被滥用。
|
||||
|
||||
## 🔮 未来展望 (Future Improvements)
|
||||
|
||||
* **流式响应 (Streaming)**: 实现打字机效果,进一步提升用户感知速度。
|
||||
* **持久化缓存**: 使用 Redis 等工具替代内存缓存,使缓存数据在服务器重启后依然有效。
|
||||
* **用户历史记录**: 增加用户查询历史的功能。
|
||||
* **单词发音**: 利用文本转语音 (TTS) API,增加点击音标即可发音的功能。
|
||||
* **混合词典模式**: 预先计算 2万个最常用单词并存入本地数据库,实现绝大多数查询的“绝对零延迟”。
|
||||
7
config.yaml
Normal file
7
config.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
api:
|
||||
url: "https://openrouter.ai/api/v1/chat/completions"
|
||||
key: "************************************************************"
|
||||
model: "google/gemma-3n-e4b-it"
|
||||
|
||||
validation:
|
||||
max_input_chars: 30
|
||||
BIN
dictionary.db
Normal file
BIN
dictionary.db
Normal file
Binary file not shown.
8
go.mod
Normal file
8
go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module ai-dictionary
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
6
go.sum
Normal file
6
go.sum
Normal file
@@ -0,0 +1,6 @@
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
263
main.go
Normal file
263
main.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// --- 数据结构 ---
|
||||
type APIConfig struct {
|
||||
URL string `yaml:"url"`
|
||||
Key string `yaml:"key"`
|
||||
Model string `yaml:"model"`
|
||||
}
|
||||
type ValidationConfig struct {
|
||||
MaxInputChars int `yaml:"max_input_chars"`
|
||||
}
|
||||
type Config struct {
|
||||
API APIConfig `yaml:"api"`
|
||||
Validation ValidationConfig `yaml:"validation"`
|
||||
}
|
||||
type CompactDef struct {
|
||||
POS string `json:"pos"`
|
||||
M string `json:"m"`
|
||||
EX string `json:"ex"`
|
||||
}
|
||||
type CompactResponse struct {
|
||||
P string `json:"p"`
|
||||
Defs []CompactDef `json:"defs"`
|
||||
}
|
||||
|
||||
const dbFile = "./dictionary.db"
|
||||
const promptsDir = "./prompts"
|
||||
|
||||
var db *sql.DB
|
||||
var config Config
|
||||
var promptCache = make(map[string]string)
|
||||
var goldenDictTemplate *template.Template
|
||||
|
||||
func loadPrompts() {
|
||||
files, err := os.ReadDir(promptsDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read prompts directory '%s': %v", promptsDir, err)
|
||||
}
|
||||
for _, file := range files {
|
||||
if !file.IsDir() && strings.HasSuffix(file.Name(), ".txt") {
|
||||
content, err := os.ReadFile(filepath.Join(promptsDir, file.Name()))
|
||||
if err != nil {
|
||||
log.Printf("Warning: could not read prompt file %s: %v", file.Name(), err)
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSuffix(file.Name(), ".txt")
|
||||
promptCache[key] = string(content)
|
||||
log.Printf("Loaded prompt template: '%s'", key)
|
||||
}
|
||||
}
|
||||
if len(promptCache) == 0 {
|
||||
log.Println("Warning: No prompt templates were loaded. Please check the 'prompts' directory.")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 关键修复:确保在main函数的最开始调用 loadPrompts()
|
||||
loadPrompts()
|
||||
|
||||
// 加载YAML配置
|
||||
configFile, err := os.ReadFile("config.yaml")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read config.yaml: %v.", err)
|
||||
}
|
||||
err = yaml.Unmarshal(configFile, &config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse config.yaml: %v", err)
|
||||
}
|
||||
if config.API.Key == "sk-or-your-key-here" || config.API.Key == "" {
|
||||
log.Fatal("API key is not set in config.yaml.")
|
||||
}
|
||||
if config.Validation.MaxInputChars <= 0 {
|
||||
config.Validation.MaxInputChars = 50
|
||||
}
|
||||
log.Println("Configuration loaded successfully.")
|
||||
|
||||
// 加载HTML模板
|
||||
goldenDictTemplate = template.Must(template.ParseFiles("templates/goldendict.html"))
|
||||
log.Println("HTML template loaded successfully.")
|
||||
|
||||
// 初始化数据库
|
||||
db, err = sql.Open("sqlite3", dbFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open database: %v", err)
|
||||
}
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS cache ( "word_key" TEXT NOT NULL PRIMARY KEY, "definition" TEXT, "created_at" DATETIME DEFAULT CURRENT_TIMESTAMP );`
|
||||
_, err = db.Exec(createTableSQL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
log.Println("Database initialized successfully.")
|
||||
|
||||
// 注册路由
|
||||
http.HandleFunc("/api/config", configHandler())
|
||||
http.HandleFunc("/api/lookup", lookupHandler())
|
||||
http.HandleFunc("/golden-dict", goldenDictHandler())
|
||||
http.Handle("/", http.FileServer(http.Dir("./static")))
|
||||
|
||||
fmt.Println("Server starting on http://localhost:8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
func getDefinition(word, sourceLang, targetLang string) ([]byte, error) {
|
||||
wordKey := fmt.Sprintf("%s-%s:%s", sourceLang, targetLang, word)
|
||||
|
||||
var cachedDefinition string
|
||||
err := db.QueryRow("SELECT definition FROM cache WHERE word_key = ?", wordKey).Scan(&cachedDefinition)
|
||||
if err == nil {
|
||||
log.Printf("Cache hit from SQLite for key: %s", wordKey)
|
||||
return []byte(cachedDefinition), nil
|
||||
}
|
||||
|
||||
log.Printf("Cache miss for key: %s. Calling AI API...", wordKey)
|
||||
|
||||
promptKey := fmt.Sprintf("%s-%s", sourceLang, targetLang)
|
||||
promptTemplate, ok := promptCache[promptKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported language pair: %s", promptKey)
|
||||
}
|
||||
prompt := strings.Replace(promptTemplate, "${word}", word, -1)
|
||||
|
||||
requestBody, _ := json.Marshal(map[string]interface{}{"model": config.API.Model, "messages": []map[string]string{{"role": "user", "content": prompt}}})
|
||||
req, _ := http.NewRequest("POST", config.API.URL, bytes.NewBuffer(requestBody))
|
||||
req.Header.Set("Authorization", "Bearer "+config.API.Key)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("HTTP-Referer", "http://localhost")
|
||||
req.Header.Set("X-Title", "AI Dictionary")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, apiErr := client.Do(req)
|
||||
if apiErr != nil {
|
||||
return nil, fmt.Errorf("failed to call AI API: %v", apiErr)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var aiApiResponse struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &aiApiResponse); err != nil || len(aiApiResponse.Choices) == 0 {
|
||||
return nil, fmt.Errorf("failed to parse AI response: %s", string(body))
|
||||
}
|
||||
contentJSON := aiApiResponse.Choices[0].Message.Content
|
||||
if strings.Contains(contentJSON, "```") {
|
||||
startIndex := strings.Index(contentJSON, "{")
|
||||
endIndex := strings.LastIndex(contentJSON, "}")
|
||||
if startIndex != -1 && endIndex != -1 && startIndex < endIndex {
|
||||
contentJSON = contentJSON[startIndex : endIndex+1]
|
||||
}
|
||||
}
|
||||
|
||||
_, err = db.Exec("INSERT INTO cache (word_key, definition) VALUES (?, ?)", wordKey, contentJSON)
|
||||
if err != nil {
|
||||
log.Printf("Failed to insert into cache: %v", err)
|
||||
}
|
||||
|
||||
return []byte(contentJSON), nil
|
||||
}
|
||||
|
||||
func goldenDictHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var word, sourceLang, targetLang string
|
||||
|
||||
if r.URL.Query().Has("word") {
|
||||
word = r.URL.Query().Get("word")
|
||||
sourceLang = r.URL.Query().Get("source")
|
||||
targetLang = r.URL.Query().Get("target")
|
||||
} else {
|
||||
queryParts := strings.SplitN(r.URL.RawQuery, "-", 3)
|
||||
if len(queryParts) == 3 {
|
||||
sourceLang = queryParts[0]
|
||||
targetLang = queryParts[1]
|
||||
word = queryParts[2]
|
||||
}
|
||||
}
|
||||
|
||||
if word == "" || sourceLang == "" || targetLang == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
goldenDictTemplate.Execute(w, map[string]interface{}{"Error": "Invalid request format."})
|
||||
return
|
||||
}
|
||||
|
||||
definitionJSON, err := getDefinition(word, sourceLang, targetLang)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
goldenDictTemplate.Execute(w, map[string]interface{}{"Error": err.Error(), "Word": word})
|
||||
return
|
||||
}
|
||||
|
||||
var responseData CompactResponse
|
||||
err = json.Unmarshal(definitionJSON, &responseData)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
goldenDictTemplate.Execute(w, map[string]interface{}{"Error": "Failed to parse AI JSON response.", "Word": word})
|
||||
return
|
||||
}
|
||||
|
||||
templateData := map[string]interface{}{"Word": word, "Result": responseData, "Error": nil}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
goldenDictTemplate.Execute(w, templateData)
|
||||
}
|
||||
}
|
||||
|
||||
func lookupHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
word := r.URL.Query().Get("word")
|
||||
sourceLang := r.URL.Query().Get("source")
|
||||
targetLang := r.URL.Query().Get("target")
|
||||
if word == "" || sourceLang == "" || targetLang == "" {
|
||||
http.Error(w, "Missing required parameters (word, source, target)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
maxChars := config.Validation.MaxInputChars
|
||||
if len([]rune(word)) > maxChars {
|
||||
http.Error(w, fmt.Sprintf("Input too long. Max %d characters allowed.", maxChars), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
definitionJSON, err := getDefinition(word, sourceLang, targetLang)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(definitionJSON)
|
||||
}
|
||||
}
|
||||
|
||||
func configHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
availablePairs := make(map[string][]string)
|
||||
for key := range promptCache {
|
||||
parts := strings.Split(key, "-")
|
||||
if len(parts) == 2 {
|
||||
source, target := parts[0], parts[1]
|
||||
availablePairs[source] = append(availablePairs[source], target)
|
||||
}
|
||||
}
|
||||
frontendConfig := map[string]interface{}{"max_input_chars": config.Validation.MaxInputChars, "available_pairs": availablePairs}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(frontendConfig)
|
||||
}
|
||||
}
|
||||
1
prompts/ar-zh.txt
Normal file
1
prompts/ar-zh.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Arabic-Chinese dictionary for learners. For the Arabic word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key should contain a romanized pronunciation. The "m" key must be the Chinese meaning, "ex" must be a simple Arabic example. Use this format: {"p":"pronunciation","defs":[{"pos":"part_of_speech","m":"meaning_in_chinese","ex":"example_sentence_in_arabic"}]}.
|
||||
1
prompts/de-en.txt
Normal file
1
prompts/de-en.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical German-English dictionary for learners. For the German word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription if available. The "m" key must be the English meaning, "ex" must be a simple German example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_english","ex":"example_sentence_in_german"}]}.
|
||||
1
prompts/de-zh.txt
Normal file
1
prompts/de-zh.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical German-Chinese dictionary for learners. For the German word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription if available. The "m" key must be the Chinese meaning, "ex" must be a simple German example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_chinese","ex":"example_sentence_in_german"}]}.
|
||||
1
prompts/en-ar.txt
Normal file
1
prompts/en-ar.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical English-Arabic dictionary for learners. For the English word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription. The "m" key must be the Arabic meaning, "ex" must be a simple English example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_arabic","ex":"example_sentence_in_english"}]}.
|
||||
1
prompts/en-de.txt
Normal file
1
prompts/en-de.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical English-German dictionary for learners. For the English word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription. The "m" key must be the German meaning, "ex" must be a simple English example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_german","ex":"example_sentence_in_english"}]}.
|
||||
1
prompts/en-en.txt
Normal file
1
prompts/en-en.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical English monolingual dictionary. For the English word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription. The "m" key must be the English meaning, "ex" must be a simple English example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_english","ex":"example_sentence_in_english"}]}.
|
||||
1
prompts/en-es.txt
Normal file
1
prompts/en-es.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical English-Spanish dictionary for learners. For the English word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription. The "m" key must be the Spanish meaning, "ex" must be a simple English example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_spanish","ex":"example_sentence_in_english"}]}.
|
||||
1
prompts/en-fr.txt
Normal file
1
prompts/en-fr.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical English-French dictionary for learners. For the English word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription. The "m" key must be the French meaning, "ex" must be a simple English example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_french","ex":"example_sentence_in_english"}]}.
|
||||
1
prompts/en-ja.txt
Normal file
1
prompts/en-ja.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical English-Japanese dictionary for learners. For the English word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription. The "m" key must be the Japanese meaning, "ex" must be a simple English example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_japanese","ex":"example_sentence_in_english"}]}.
|
||||
1
prompts/en-pt.txt
Normal file
1
prompts/en-pt.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical English-Portuguese dictionary for learners. For the English word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription. The "m" key must be the Portuguese meaning, "ex" must be a simple English example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_portuguese","ex":"example_sentence_in_english"}]}.
|
||||
1
prompts/en-ru.txt
Normal file
1
prompts/en-ru.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical English-Russian dictionary for learners. For the English word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription. The "m" key must be the Russian meaning, "ex" must be a simple English example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_russian","ex":"example_sentence_in_english"}]}.
|
||||
1
prompts/en-zh.txt
Normal file
1
prompts/en-zh.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical English-Chinese dictionary for learners. For the English word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription. The "m" key must be the Chinese meaning, "ex" must be a simple English example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_chinese","ex":"example_sentence_in_english"}]}.
|
||||
1
prompts/es-en.txt
Normal file
1
prompts/es-en.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Spanish-English dictionary for learners. For the Spanish word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription if available. The "m" key must be the English meaning, "ex" must be a simple Spanish example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_english","ex":"example_sentence_in_spanish"}]}.
|
||||
1
prompts/es-es.txt
Normal file
1
prompts/es-es.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Spanish monolingual dictionary. For the Spanish word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription if available. The "m" key must be the Spanish meaning, "ex" must be a simple Spanish example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_spanish","ex":"example_sentence_in_spanish"}]}.
|
||||
1
prompts/es-zh.txt
Normal file
1
prompts/es-zh.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Spanish-Chinese dictionary for learners. For the Spanish word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription if available. The "m" key must be the Chinese meaning, "ex" must be a simple Spanish example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_chinese","ex":"example_sentence_in_spanish"}]}.
|
||||
1
prompts/fr-en.txt
Normal file
1
prompts/fr-en.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical French-English dictionary for learners. For the French word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription if available. The "m" key must be the English meaning, "ex" must be a simple French example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_english","ex":"example_sentence_in_french"}]}.
|
||||
1
prompts/fr-zh.txt
Normal file
1
prompts/fr-zh.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical French-Chinese dictionary for learners. For the French word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription if available. The "m" key must be the Chinese meaning, "ex" must be a simple French example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_chinese","ex":"example_sentence_in_french"}]}.
|
||||
1
prompts/ja-zh.txt
Normal file
1
prompts/ja-zh.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Japanese-Chinese dictionary for learners. For the Japanese word "${word}", return compact JSON. The "p" key can be the Romaji or Hiragana pronunciation. The "m" key must be the Chinese meaning, "ex" must be a simple Japanese example. Use this format: {"p":"pronunciation","defs":[{"pos":"part_of_speech","m":"meaning_in_chinese","ex":"example_sentence_in_japanese"}]}.
|
||||
1
prompts/pt-zh.txt
Normal file
1
prompts/pt-zh.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Portuguese-Chinese dictionary for learners. For the Portuguese word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription if available. The "m" key must be the Chinese meaning, "ex" must be a simple Portuguese example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_chinese","ex":"example_sentence_in_portuguese"}]}.
|
||||
1
prompts/ru-zh.txt
Normal file
1
prompts/ru-zh.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Russian-Chinese dictionary for learners. For the Russian word "${word}", return compact JSON. OMIT archaic or extremely rare usages. The "p" key MUST contain the IPA transcription if available. The "m" key must be the Chinese meaning, "ex" must be a simple Russian example. Use this format: {"p":"IPA_notation","defs":[{"pos":"part_of_speech","m":"meaning_in_chinese","ex":"example_sentence_in_russian"}]}.
|
||||
1
prompts/zh-ar.txt
Normal file
1
prompts/zh-ar.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Chinese-Arabic dictionary for learners. For the Chinese word "${word}", return compact JSON. The "p" key MUST contain the Pinyin pronunciation. The "m" key must be the Arabic meaning, "ex" must be a simple Chinese example. Use this format: {"p":"pinyin_pronunciation","defs":[{"pos":"part_of_speech","m":"meaning_in_arabic","ex":"example_sentence_in_chinese"}]}.
|
||||
1
prompts/zh-de.txt
Normal file
1
prompts/zh-de.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Chinese-German dictionary for learners. For the Chinese word "${word}", return compact JSON. The "p" key MUST contain the Pinyin pronunciation. The "m" key must be the German meaning, "ex" must be a simple Chinese example. Use this format: {"p":"pinyin_pronunciation","defs":[{"pos":"part_of_speech","m":"meaning_in_german","ex":"example_sentence_in_chinese"}]}.
|
||||
1
prompts/zh-en.txt
Normal file
1
prompts/zh-en.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Chinese-English dictionary for learners. For the Chinese word "${word}", return compact JSON. The "p" key MUST contain the Pinyin pronunciation. The "m" key must be the English meaning, "ex" must be a simple Chinese example. Use this format: {"p":"pinyin_pronunciation","defs":[{"pos":"part_of_speech","m":"meaning_in_english","ex":"example_sentence_in_chinese"}]}.
|
||||
1
prompts/zh-es.txt
Normal file
1
prompts/zh-es.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Chinese-Spanish dictionary for learners. For the Chinese word "${word}", return compact JSON. The "p" key MUST contain the Pinyin pronunciation. The "m" key must be the Spanish meaning, "ex" must be a simple Chinese example. Use this format: {"p":"pinyin_pronunciation","defs":[{"pos":"part_of_speech","m":"meaning_in_spanish","ex":"example_sentence_in_chinese"}]}.
|
||||
1
prompts/zh-fr.txt
Normal file
1
prompts/zh-fr.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Chinese-French dictionary for learners. For the Chinese word "${word}", return compact JSON. The "p" key MUST contain the Pinyin pronunciation. The "m" key must be the French meaning, "ex" must be a simple Chinese example. Use this format: {"p":"pinyin_pronunciation","defs":[{"pos":"part_of_speech","m":"meaning_in_french","ex":"example_sentence_in_chinese"}]}.
|
||||
1
prompts/zh-ja.txt
Normal file
1
prompts/zh-ja.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Chinese-Japanese dictionary for learners. For the Chinese word "${word}", return compact JSON. The "p" key MUST contain the Pinyin pronunciation. The "m" key must be the Japanese meaning (Kanji/Kana), "ex" must be a simple Chinese example. Use this format: {"p":"pinyin_pronunciation","defs":[{"pos":"part_of_speech","m":"meaning_in_japanese","ex":"example_sentence_in_chinese"}]}.
|
||||
1
prompts/zh-pt.txt
Normal file
1
prompts/zh-pt.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Chinese-Portuguese dictionary for learners. For the Chinese word "${word}", return compact JSON. The "p" key MUST contain the Pinyin pronunciation. The "m" key must be the Portuguese meaning, "ex" must be a simple Chinese example. Use this format: {"p":"pinyin_pronunciation","defs":[{"pos":"part_of_speech","m":"meaning_in_portuguese","ex":"example_sentence_in_chinese"}]}.
|
||||
1
prompts/zh-ru.txt
Normal file
1
prompts/zh-ru.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are an editor for a concise, modern, practical Chinese-Russian dictionary for learners. For the Chinese word "${word}", return compact JSON. The "p" key MUST contain the Pinyin pronunciation. The "m" key must be the Russian meaning, "ex" must be a simple Chinese example. Use this format: {"p":"pinyin_pronunciation","defs":[{"pos":"part_of_speech","m":"meaning_in_russian","ex":"example_sentence_in_chinese"}]}.
|
||||
133
static/app.js
Normal file
133
static/app.js
Normal file
@@ -0,0 +1,133 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const searchButton = document.getElementById('searchButton');
|
||||
const wordInput = document.getElementById('wordInput');
|
||||
const resultsDiv = document.getElementById('results');
|
||||
const sourceLangSelect = document.getElementById('sourceLang');
|
||||
const targetLangSelect = document.getElementById('targetLang');
|
||||
|
||||
// 全局配置变量,增加 available_pairs
|
||||
let appConfig = {
|
||||
max_input_chars: 50,
|
||||
available_pairs: {}
|
||||
};
|
||||
|
||||
// --- 新增:一个映射,用于在UI上显示完整的语言名称 ---
|
||||
const langNameMap = {
|
||||
"en": "英语 (English)", "zh": "中文 (Chinese)", "es": "西班牙语 (Spanish)",
|
||||
"fr": "法语 (French)", "de": "德语 (German)", "ru": "俄语 (Russian)",
|
||||
"ja": "日语 (Japanese)", "ar": "阿拉伯语 (Arabic)", "pt": "葡萄牙语 (Portuguese)",
|
||||
};
|
||||
// --- 映射结束 ---
|
||||
|
||||
// --- 新增:更新目标语言下拉菜单的函数 ---
|
||||
function updateTargetLangOptions() {
|
||||
const selectedSource = sourceLangSelect.value;
|
||||
const availableTargets = appConfig.available_pairs[selectedSource] || [];
|
||||
|
||||
// 清空当前选项
|
||||
targetLangSelect.innerHTML = '';
|
||||
|
||||
if (availableTargets.length === 0) {
|
||||
// 如果没有可用的目标语言,可以禁用或显示提示
|
||||
const option = document.createElement('option');
|
||||
option.textContent = "无可用目标";
|
||||
targetLangSelect.appendChild(option);
|
||||
targetLangSelect.disabled = true;
|
||||
} else {
|
||||
// 重新填充选项
|
||||
availableTargets.forEach(langCode => {
|
||||
const option = document.createElement('option');
|
||||
option.value = langCode;
|
||||
option.textContent = langNameMap[langCode] || langCode;
|
||||
targetLangSelect.appendChild(option);
|
||||
});
|
||||
targetLangSelect.disabled = false;
|
||||
}
|
||||
}
|
||||
// --- 函数结束 ---
|
||||
|
||||
|
||||
(async function fetchConfig() {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
if (response.ok) {
|
||||
const configData = await response.json();
|
||||
appConfig = configData;
|
||||
console.log("Successfully loaded config from server:", appConfig);
|
||||
|
||||
// --- 新增:配置加载成功后,立即更新一次目标语言菜单 ---
|
||||
updateTargetLangOptions();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Could not fetch config from server, using default values.", error);
|
||||
}
|
||||
})();
|
||||
|
||||
// --- 新增:为源语言选择框添加 onchange 事件监听器 ---
|
||||
sourceLangSelect.addEventListener('change', updateTargetLangOptions);
|
||||
|
||||
|
||||
const performSearch = async () => {
|
||||
// ... performSearch 函数本身的代码完全不用变 ...
|
||||
const word = wordInput.value.trim();
|
||||
const sourceLang = sourceLangSelect.value;
|
||||
const targetLang = targetLangSelect.value;
|
||||
if (!word || !sourceLang || !targetLang || targetLangSelect.disabled) return;
|
||||
|
||||
const maxChars = appConfig.max_input_chars;
|
||||
if (word.length > maxChars) {
|
||||
resultsDiv.innerHTML = `<p style="color: red;">输入内容过长,最多允许输入 ${maxChars} 个字符。</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
resultsDiv.innerHTML = '<p>正在查询...</p>';
|
||||
try {
|
||||
const apiUrl = `/api/lookup?word=${encodeURIComponent(word)}&source=${sourceLang}&target=${targetLang}`;
|
||||
const response = await fetch(apiUrl);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`服务器错误: ${response.status} - ${errorText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log("Received data from backend:", data);
|
||||
renderResults(data);
|
||||
} catch (error) {
|
||||
resultsDiv.innerHTML = `<p style="color: red;">查询失败: ${error.message}</p>`;
|
||||
}
|
||||
};
|
||||
|
||||
// ... 其他事件监听和 renderResults 函数保持不变 ...
|
||||
searchButton.addEventListener('click', performSearch);
|
||||
wordInput.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
|
||||
function renderResults(data) {
|
||||
const resultsDiv = document.getElementById('results');
|
||||
resultsDiv.innerHTML = '';
|
||||
let html = `<h2>${wordInput.value}</h2>`;
|
||||
html += `<div class="phonetics">`;
|
||||
if (data.p) {
|
||||
html += `<span>${data.p}</span>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
if (data.defs && Array.isArray(data.defs) && data.defs.length > 0) {
|
||||
data.defs.forEach(def => {
|
||||
html += `<div class="entry">`;
|
||||
html += `<div class="part-of-speech">${def.pos}</div>`;
|
||||
html += `<div class="definition-block">`;
|
||||
html += `<p>• ${def.m}</p>`;
|
||||
if (def.ex) {
|
||||
html += `<div class="example"><p>e.g., ${def.ex}</p></div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
html += `</div>`;
|
||||
});
|
||||
} else {
|
||||
html += `<p>未找到该词的释义。</p>`
|
||||
}
|
||||
resultsDiv.innerHTML = html;
|
||||
}
|
||||
});
|
||||
68
static/index.html
Normal file
68
static/index.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Dictionary</title>
|
||||
<style>
|
||||
/* CSS样式保持不变 */
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; color: #333; }
|
||||
h1 { color: #1a73e8; }
|
||||
#search-controls { display: flex; gap: 10px; margin-bottom: 20px; align-items: center; }
|
||||
#lang-selectors { display: flex; gap: 5px; }
|
||||
#lang-selectors select { padding: 8px; font-size: 14px; border: 1px solid #ccc; border-radius: 8px; }
|
||||
#search-container { display: flex; flex-grow: 1; gap: 10px; }
|
||||
#wordInput { flex-grow: 1; padding: 10px; font-size: 16px; border: 1px solid #ccc; border-radius: 8px; }
|
||||
#searchButton { padding: 10px 20px; font-size: 16px; background-color: #1a73e8; color: white; border: none; border-radius: 8px; cursor: pointer; }
|
||||
#searchButton:hover { background-color: #185abc; }
|
||||
#results { margin-top: 30px; }
|
||||
.entry { border-left: 3px solid #1a73e8; padding: 15px; margin-top: 20px; background-color: #f8f9fa; border-radius: 0 8px 8px 0; }
|
||||
.phonetics { font-size: 1.1em; color: #5f6368; }
|
||||
.phonetics span { margin-right: 20px; }
|
||||
.part-of-speech { font-weight: bold; font-size: 1.2em; margin-top: 10px; }
|
||||
.definition-block { margin-top: 15px; }
|
||||
.definition-block small { color: #5f6368; }
|
||||
.example { margin-top: 10px; font-style: italic; color: #3c4043; }
|
||||
.example small { color: #70757a; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>AI 词典</h1>
|
||||
|
||||
<div id="search-controls">
|
||||
<div id="lang-selectors">
|
||||
<select id="sourceLang">
|
||||
<option value="en">英语 (English)</option>
|
||||
<option value="zh">中文 (Chinese)</option>
|
||||
<option value="es">西班牙语 (Spanish)</option>
|
||||
<option value="fr">法语 (French)</option>
|
||||
<option value="de">德语 (German)</option>
|
||||
<option value="ru">俄语 (Russian)</option>
|
||||
<option value="ja">日语 (Japanese)</option>
|
||||
<option value="ar">阿拉伯语 (Arabic)</option>
|
||||
<option value="pt">葡萄牙语 (Portuguese)</option>
|
||||
</select>
|
||||
<span>→</span>
|
||||
<select id="targetLang">
|
||||
<option value="zh">中文 (Chinese)</option>
|
||||
<option value="en">英语 (English)</option>
|
||||
<option value="es">西班牙语 (Spanish)</option>
|
||||
<option value="fr">法语 (French)</option>
|
||||
<option value="de">德语 (German)</option>
|
||||
<option value="ru">俄语 (Russian)</option>
|
||||
<option value="ja">日语 (Japanese)</option>
|
||||
<option value="ar">阿拉伯语 (Arabic)</option>
|
||||
<option value="pt">葡萄牙语 (Portuguese)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="search-container">
|
||||
<input type="text" id="wordInput" placeholder="输入要查询的单词...">
|
||||
<button id="searchButton">查询</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="results"></div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
81
templates/goldendict.html
Normal file
81
templates/goldendict.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
/* 这是为GoldenDict优化的美化CSS */
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
margin: 15px;
|
||||
background-color: #ffffff;
|
||||
color: #202124;
|
||||
font-size: 16px;
|
||||
}
|
||||
.word {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
color: #1a0dab;
|
||||
}
|
||||
.phonetics {
|
||||
font-size: 1.2em;
|
||||
color: #5f6368;
|
||||
margin-top: -5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.entry {
|
||||
border-left: 3px solid #1a73e8;
|
||||
padding: 15px;
|
||||
margin-top: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
.part-of-speech {
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
color: #3c4043;
|
||||
}
|
||||
.definition-block {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.meaning {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.example {
|
||||
margin-top: 8px;
|
||||
font-style: italic;
|
||||
color: #70757a;
|
||||
}
|
||||
.error {
|
||||
color: #d93025;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{if .Error}}
|
||||
<div class="error">{{.Error}}</div>
|
||||
{{else}}
|
||||
<div class="word">{{.Word}}</div>
|
||||
{{if .Result.P}}
|
||||
<div class="phonetics">{{.Result.P}}</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Result.Defs}}
|
||||
{{range .Result.Defs}}
|
||||
<div class="entry">
|
||||
<div class="part-of-speech">{{.POS}}</div>
|
||||
<div class="definition-block">
|
||||
<div class="meaning">• {{.M}}</div>
|
||||
{{if .EX}}
|
||||
<div class="example">e.g., {{.EX}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>未找到该词的释义。</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user