diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..8ad74f78
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Normalize EOL for all files that Git considers text files.
+* text=auto eol=lf
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..40a9732e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+# Godot 4+ specific ignores
+.godot/
+.build/
+.test/
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..b1471de2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,107 @@
+
🐶 Godog 🐶
+AI Godot 桌宠
+
+简体中文 / [English](README_EN.md)
+
+## 👋 介绍
+
+🚀 🚀 🚀 一款由大语言模型驱动、Godot 制作的 AI 桌宠,旨在提供一个全能的、丰富的桌面AI宠物。你可以将其作为基础,构建你自己独特的桌宠形象和角色行为
+
+- 使用Godot:轻量(内存占用90mb)、开发简单、提供动画、控件UI 等框架
+- 兼容大量的大语言模型
+- 有预制角色功能,多种形象
+- 支持多模态
+- 在此基础上,方便构建自己的 AI 桌宠
+
+![](/img/example.png)
+
+## 📙 使用手册
+
+### ⬇️ 安装
+
+下载地址:查看 `release`
+
+### 🔑 使用
+1. 点击 `编辑预设`,填写名称,API Key,点击 **保存(修改)**
+2. 点击 `存储预设`,选中刚刚添加的预设
+3. 开始对话
+
+> [!Caution]
+> 当前仅支持 OpenAI 的视觉能力(VISION),建议使用 **gpt-4o** 模型
+> 你可以使用其他模型,但不支持多模态
+
+### ⌨️ 快捷键
+- **鼠标右键** 角色区域弹出对话框
+- 当焦点在角色窗口上,按 **ESC** 关闭软件
+
+## 🚦开发计划
+
+- [ ] 添加多语言的支持
+- [ ] 添加更多个性化角色的动画,机制,事件
+- [ ] 添加更多模型支持
+- [ ] 添加对话框对 markdown 语法渲染的支持
+- [ ] 添加语音、文件上传等多模态的能力
+- [ ] 添加更多的预设
+- [ ] 添加更多角色主题设置,方便导入
+
+## 🐶 开发自己的桌宠
+
+基于 Godot 的轻便和易学,你可以很容易构建独属于你自己的桌面宠物形象。
+
+### 🐾 步骤
+
+1. 准备一套角色帧动: 你可以查看[itch.ion](https://itch.io/game-assets)等,选择一套自己喜欢的下载,**注意版权,仅自己用不用太注意**。
+2. Godot!启动![下载](https://godotengine.org/download/windows/)
+3. 查看并学习官方文档:不需要害怕,Godot很简单,查看[Animation](https://docs.godotengine.org/en/4.2/tutorials/animation/index.html)。
+4. 开发(continue...)
+
+
+5. [导出](https://docs.godotengine.org/zh-cn/4.x/tutorials/export/index.html)
+
+### 🏗️ 项目架构
+
+```css
+- root(Window)
+ - Globals(Node)
+ - App(Node)
+ - Canvas(Node2D) - 角色部分
+ - Grapic(Node2D) - 显示区域 和 角色管理
+ - Dialogue(Control) - 对话显示
+ - Send(Window) - 发送消息
+ - TabContainer(TabContainer)
+ - 对话栏(Control)
+ - 存储预设(Control)
+ - 编辑预设(Control)
+ - Model(PanelContainer)
+ - Parament(PanelContainer)
+```
+
+### 🔊 注意
+
+> [!Caution]
+> 1. 开发使用的godot版本号为 4.2.2 stable,Godot4 应该都是可以的(未测试)
+> 2. 导入角色素材记得合适的缩放,同时不忘了调整 显示区域(ClickPolygon) 的大小
+> 3. Godot右上角 选择**兼容模式**
+
+## 🤝 贡献
+
+
+项目目前处于开发阶段,还存在许多 Bug,我正着手修复。
+欢迎任何贡献,即使细微。
+添加角色素材等等
+
+## ©️ 版权和授权
+
+你可以使用项目中的任何代码片段,但不能直接将完整项目打包上架市场。
+同时,需要注意项目中使用到的一些资产的版权信息。
+
+
+## ❤️ 鸣谢
+
+多亏了如下开源的项目和精彩绝伦的资产,才有了该项目:
+
+- [ChatGPT-stream-for-Godot-4](https://github.com/oceanbuilders/ChatGPT-stream-for-Godot-4) - stream输出的支持
+- [godot-click-through-transparent-window](https://github.com/atadenizoktay/godot-click-through-transparent-window) - 透明窗口
+- [tech-dungeon-roguelite](https://trevor-pupkin.itch.io/tech-dungeon-roguelite) - 角色资产
+- [tiny-swords](https://pixelfrog-assets.itch.io/tiny-swords) - 角色资产
+- [dino-characters](https://arks.itch.io/dino-characters) - 角色资产
diff --git a/README_EN.md b/README_EN.md
new file mode 100644
index 00000000..7a4f0fce
--- /dev/null
+++ b/README_EN.md
@@ -0,0 +1,104 @@
+🐶 Godog 🐶
+AI Godot Desktop Pet
+
+[简体中文](README.md) / English
+
+## 👋 Introduction
+
+🚀 🚀 🚀 A desktop AI pet powered by a large language model and made with Godot. This project aims to provide a versatile and rich desktop AI pet. You can use it as a foundation to create your own unique pet image and character behavior.
+
+- Using Godot: lightweight (90MB memory usage), simple development, provides animation, control UI framework, etc.
+- Compatible with many large language models
+- Pre-made character feature with multiple appearances
+- Supports multi-modal capabilities
+- Easy to build your own AI desktop pet on this basis
+
+![](/img/example.png)
+
+## 📙 User Manual
+
+### ⬇️ Installation
+
+Download: Check `release`
+
+### 🔑 Usage
+1. Click `Edit Preset`, fill in the name and API Key, and click **Save (Modify)**
+2. Click `Store Preset` and select the preset you just added
+3. Start the conversation
+
+> [!Caution]
+> Currently, only OpenAI's vision capability (VISION) is supported. It is recommended to use the **gpt-4o** model.
+> You can use other models, but multi-modal capabilities are not supported.
+
+### ⌨️ Shortcuts
+- **Right-click** on the character area to pop up the dialog box
+- When the focus is on the character window, press **ESC** to close the software
+
+## 🚦 Development Plan
+
+- [ ] Add support for multiple languages
+- [ ] Add more personalized character animations, mechanisms, events
+- [ ] Add support for more models
+- [ ] Add support for Markdown syntax rendering in the dialog box
+- [ ] Add multi-modal capabilities such as voice, file upload, etc.
+- [ ] Add more presets
+- [ ] Add more character theme settings for easy import
+
+## 🐶 Developing Your Own Desktop Pet
+
+With Godot's lightweight and easy-to-learn nature, you can easily create your own unique desktop pet image.
+
+### 🐾 Steps
+
+1. Prepare a set of character frames: You can check [itch.io](https://itch.io/game-assets), choose a set you like and download it, **be mindful of copyright if not for personal use**.
+2. Godot! Start! [Download](https://godotengine.org/download/windows/)
+3. Review and learn the official documentation: Don’t be afraid, Godot is simple. Check out [Animation](https://docs.godotengine.org/en/4.2/tutorials/animation/index.html).
+4. Development (continue...)
+
+5. [Export](https://docs.godotengine.org/en/4.x/tutorials/export/index.html)
+
+### 🏗️ Project Structure
+
+```css
+- root(Window)
+ - Globals(Node)
+ - App(Node)
+ - Canvas(Node2D) - Character part
+ - Graphic(Node2D) - Display area and character management
+ - Dialogue(Control) - Dialog display
+ - Send(Window) - Send message
+ - TabContainer(TabContainer)
+ - Dialog tab(Control)
+ - Store preset(Control)
+ - Edit preset(Control)
+ - Model(PanelContainer)
+ - Parameters(PanelContainer)
+```
+
+### 🔊 Notes
+
+> [!Caution]
+> 1. The Godot version used for development is 4.2.2 stable, Godot4 should work as well (not tested)
+> 2. Remember to scale the imported character assets appropriately and adjust the display area (ClickPolygon) size
+> 3. In the upper right corner of Godot, select **Compatibility Mode**
+
+## 🤝 Contributions
+
+The project is currently under development and has many bugs that I am working to fix.
+Any contributions are welcome, even small ones.
+Adding character assets, etc.
+
+## ©️ License
+
+You can use any code snippet from the project, but you cannot directly package and sell the complete project.
+Additionally, pay attention to the copyright information of some assets used in the project.
+
+## ❤️ Acknowledgements
+
+Thanks to the following open-source projects and amazing assets, without which this project would not exist:
+
+- [ChatGPT-stream-for-Godot-4](https://github.com/oceanbuilders/ChatGPT-stream-for-Godot-4) - Support for stream output
+- [godot-click-through-transparent-window](https://github.com/atadenizoktay/godot-click-through-transparent-window) - Transparent window
+- [tech-dungeon-roguelite](https://trevor-pupkin.itch.io/tech-dungeon-roguelite) - Character assets
+- [tiny-swords](https://pixelfrog-assets.itch.io/tiny-swords) - Character assets
+- [dino-characters](https://arks.itch.io/dino-characters) - Character assets
diff --git a/addons/HTTPSSEClient/HTTPSSEClient_modified.gd b/addons/HTTPSSEClient/HTTPSSEClient_modified.gd
new file mode 100644
index 00000000..9612a936
--- /dev/null
+++ b/addons/HTTPSSEClient/HTTPSSEClient_modified.gd
@@ -0,0 +1,144 @@
+class_name HTTPSSEClient
+extends Node
+
+signal new_sse_event
+signal connected
+signal connection_error(error)
+
+
+var httpclient = HTTPClient.new()
+var is_connected = false
+
+var domain
+var url_after_domain
+var headers : PackedStringArray
+var body: String
+var port
+var told_to_connect = false
+var connection_in_progress = false
+var request_in_progress = false
+var is_requested = false
+var response_body = PackedByteArray()
+
+var ai_status_message
+
+func connect_to_host(domain : String, url_after_domain : String, headers: PackedStringArray, body: String, ai_message: ChatMessageAI, port : int = -1):
+ self.domain = domain
+ self.url_after_domain = url_after_domain
+ self.port = port
+ self.headers = headers
+ self.body = body
+ told_to_connect = true
+ ai_status_message = ai_message
+
+func close_connection():
+ httpclient.close()
+ is_connected = false
+ told_to_connect = false
+ connection_in_progress = false
+ request_in_progress = false
+ is_requested = false
+
+func attempt_to_connect():
+ var err = httpclient.connect_to_host(domain, port)
+ if err == OK:
+ emit_signal("connected")
+ is_connected = true
+ else:
+ emit_signal("connection_error", str(err))
+
+func attempt_to_request(httpclient_status):
+ if httpclient_status == HTTPClient.STATUS_CONNECTING or httpclient_status == HTTPClient.STATUS_RESOLVING:
+ return
+
+ if httpclient_status == HTTPClient.STATUS_CONNECTED:
+ var err = httpclient.request(HTTPClient.METHOD_POST, url_after_domain, headers, body)
+ if err == OK:
+ is_requested = true
+
+func _process(delta):
+ if !told_to_connect:
+ return
+
+ if !is_connected:
+ if !connection_in_progress:
+ attempt_to_connect()
+ connection_in_progress = true
+ return
+
+ httpclient.poll()
+ var httpclient_status = httpclient.get_status()
+ if !is_requested:
+ if !request_in_progress:
+ attempt_to_request(httpclient_status)
+ return
+
+ var httpclient_has_response = httpclient.has_response()
+
+ if httpclient_has_response or httpclient_status == HTTPClient.STATUS_BODY:
+ var response_headers = httpclient.get_response_headers_as_dictionary()
+
+ httpclient.poll()
+ var chunk = httpclient.read_response_body_chunk()
+ if(chunk.size() == 0):
+ return
+ else:
+ # each chunk sometimes can contain more than one delta of data.
+ response_body = chunk
+
+ var string_body = response_body.get_string_from_utf8()
+ if string_body:
+ var partial_reply = get_open_ai_events_data(string_body)
+ emit_signal("new_sse_event", partial_reply, ai_status_message)
+
+
+# Since each chunk sometimes can contain more than one delta of data this
+# function will iterate through each "data" block found and extract its
+# content concatenating it in an array.
+func get_open_ai_events_data(string_body: String) -> Array:
+ var results = []
+ string_body = string_body.strip_edges() # Remove leading and trailing whitespaces, including new lines
+ var data_entries = string_body.split("\n\n") # Split the body into individual data entries
+
+ for entry in data_entries:
+ if entry == "data: [DONE]":
+ results.append("[DONE]")
+ continue
+
+ entry = entry.replace("data: ", "")
+ print("Processing entry: ", entry) # Debugging line
+ var parsed_data = JSON.parse_string(entry)
+ if parsed_data == null:
+ printerr("Error: Received data is not a valid JSON object")
+ results.append("[ERROR]")
+ else:
+ if "choices" in parsed_data and parsed_data["choices"].size() > 0:
+ var choices = parsed_data["choices"][0]
+ if "delta" in choices:
+ var delta = choices["delta"]
+ if "content" in delta:
+ results.append(delta["content"])
+ elif delta.is_empty():
+ results.append("[EMPTY DELTA]")
+ else:
+ printerr("Error: 'content' field not found in the 'delta'")
+ results.append("[ERROR]")
+ else:
+ printerr("Error: 'delta' field not found in the first choice")
+ results.append("[ERROR]")
+ else:
+ printerr("Error: 'choices' field not found in the received data")
+ results.append("[ERROR]")
+
+ return results
+
+
+func _exit_tree():
+ if httpclient:
+ httpclient.close()
+
+func _notification(what):
+ if what == NOTIFICATION_WM_CLOSE_REQUEST:
+ if httpclient:
+ httpclient.close()
+ get_tree().quit()
diff --git a/addons/HTTPSSEClient/HTTPSSEClient_original.txt b/addons/HTTPSSEClient/HTTPSSEClient_original.txt
new file mode 100644
index 00000000..f4783cc4
--- /dev/null
+++ b/addons/HTTPSSEClient/HTTPSSEClient_original.txt
@@ -0,0 +1,119 @@
+extends Node
+
+signal new_sse_event(headers, event, data)
+signal connected
+signal connection_error(error)
+
+const event_tag = "event:"
+const data_tag = "data:"
+const continue_internal = "continue_internal"
+
+var httpclient = HTTPClient.new()
+var is_connected = false
+
+var domain
+var url_after_domain
+var port
+var use_ssl
+var verify_host
+var told_to_connect = false
+var connection_in_progress = false
+var request_in_progress = false
+var is_requested = false
+var response_body = PoolByteArray()
+
+func connect_to_host(domain : String, url_after_domain : String, port : int = -1, use_ssl : bool = false, verify_host : bool = true):
+ self.domain = domain
+ self.url_after_domain = url_after_domain
+ self.port = port
+ self.use_ssl = use_ssl
+ self.verify_host = verify_host
+ told_to_connect = true
+
+func attempt_to_connect():
+ var err = httpclient.connect_to_host(domain, port, use_ssl, verify_host)
+ if err == OK:
+ emit_signal("connected")
+ is_connected = true
+ else:
+ emit_signal("connection_error", str(err))
+
+func attempt_to_request(httpclient_status):
+ if httpclient_status == HTTPClient.STATUS_CONNECTING or httpclient_status == HTTPClient.STATUS_RESOLVING:
+ return
+
+ if httpclient_status == HTTPClient.STATUS_CONNECTED:
+ var err = httpclient.request(HTTPClient.METHOD_POST, url_after_domain, ["Accept: text/event-stream"])
+ if err == OK:
+ is_requested = true
+
+func _process(delta):
+ if !told_to_connect:
+ return
+
+ if !is_connected:
+ if !connection_in_progress:
+ attempt_to_connect()
+ connection_in_progress = true
+ return
+
+ httpclient.poll()
+ var httpclient_status = httpclient.get_status()
+ if !is_requested:
+ if !request_in_progress:
+ attempt_to_request(httpclient_status)
+ return
+
+ var httpclient_has_response = httpclient.has_response()
+
+ if httpclient_has_response or httpclient_status == HTTPClient.STATUS_BODY:
+ var headers = httpclient.get_response_headers_as_dictionary()
+
+ httpclient.poll()
+ var chunk = httpclient.read_response_body_chunk()
+ if(chunk.size() == 0):
+ return
+ else:
+ response_body = response_body + chunk
+
+ var body = response_body.get_string_from_utf8()
+ if body:
+ var event_data = get_event_data(body)
+ if event_data.event != "keep-alive" and event_data.event != continue_internal:
+ var result = JSON.parse(event_data.data).result
+ if response_body.size() > 0 and result: # stop here if the value doesn't parse
+ response_body.resize(0)
+ emit_signal("new_sse_event", headers, event_data.event, result)
+ else:
+ if event_data.event != continue_internal:
+ response_body.resize(0)
+
+func get_event_data(body : String) -> Dictionary:
+ var result = {}
+ var event_idx = body.find(event_tag)
+ if event_idx == -1:
+ result["event"] = continue_internal
+ return result
+ assert(event_idx != -1)
+ var data_idx = body.find(data_tag, event_idx + event_tag.length())
+ assert(data_idx != -1)
+ var event = body.substr(event_idx, data_idx)
+ event = event.replace(event_tag, "").strip_edges()
+ assert(event)
+ assert(event.length() > 0)
+ result["event"] = event
+ var data = body.right(data_idx + data_tag.length()).strip_edges()
+ assert(data)
+ assert(data.length() > 0)
+ result["data"] = data
+ return result
+
+func _exit_tree():
+ if httpclient:
+ httpclient.close()
+
+func _notification(what):
+ if what == MainLoop.NOTIFICATION_WM_QUIT_REQUEST:
+ if httpclient:
+ httpclient.close()
+ get_tree().quit()
diff --git a/addons/HTTPSSEClient/LICENSE.md b/addons/HTTPSSEClient/LICENSE.md
new file mode 100644
index 00000000..9354ed87
--- /dev/null
+++ b/addons/HTTPSSEClient/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Kyle Szklenski
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/addons/HTTPSSEClient/httpsseclient_plugin.gd b/addons/HTTPSSEClient/httpsseclient_plugin.gd
new file mode 100644
index 00000000..585377cd
--- /dev/null
+++ b/addons/HTTPSSEClient/httpsseclient_plugin.gd
@@ -0,0 +1,10 @@
+@tool
+extends EditorPlugin
+
+func _enter_tree():
+ add_custom_type("HTTPSSEClient", "Node", preload("res://addons/HTTPSSEClient/HTTPSSEClient_modified.gd"), preload("res://addons/HTTPSSEClient/icon.png"))
+ pass
+
+func _exit_tree():
+ remove_custom_type("HTTPSSEClient")
+ pass
diff --git a/addons/HTTPSSEClient/icon.png b/addons/HTTPSSEClient/icon.png
new file mode 100644
index 00000000..597d001a
Binary files /dev/null and b/addons/HTTPSSEClient/icon.png differ
diff --git a/addons/HTTPSSEClient/icon.png.import b/addons/HTTPSSEClient/icon.png.import
new file mode 100644
index 00000000..232e8917
--- /dev/null
+++ b/addons/HTTPSSEClient/icon.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://doh7ebyllxgs2"
+path="res://.godot/imported/icon.png-2ea94e924d0ead3d0aefe50938b80d4c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/HTTPSSEClient/icon.png"
+dest_files=["res://.godot/imported/icon.png-2ea94e924d0ead3d0aefe50938b80d4c.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/HTTPSSEClient/plugin.cfg b/addons/HTTPSSEClient/plugin.cfg
new file mode 100644
index 00000000..28538254
--- /dev/null
+++ b/addons/HTTPSSEClient/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="HTTPSSEClient"
+description="An HTTPClient-based implementation that supports server-sent events, effectively enabling push notifications in Godot, using GDScript."
+author="Kyle Szklenski"
+version="1.0"
+script="httpsseclient_plugin.gd"
diff --git a/app.gd b/app.gd
new file mode 100644
index 00000000..ae401bce
--- /dev/null
+++ b/app.gd
@@ -0,0 +1,172 @@
+extends Node
+
+# model request setting
+var api_key :String = ""
+var model :String = ""
+var host :String = "https://api.openai.com"
+var path :String = "/v1/chat/completions"
+
+# model paramenter setting
+var max_tokens :int = 3000
+var history_count :int = 5
+var temperature :float = 0.5
+var stream :bool = true
+var top_p :float
+var system_message :Dictionary
+
+var chat = []
+var request_url :String = host+path
+var headers = ["Authorization: Bearer " + api_key, "Content-Type: application/json"]
+
+@onready var canvas: Node2D = $Canvas
+@onready var chat_message_ai: ChatMessageAI = find_child("ChatMessageAI")
+
+var http_request : HTTPRequest
+var httpsse_client: HTTPSSEClient
+
+func _ready():
+ Globals.send_button_press.connect(_on_Btn_send)
+ Globals.update_request.connect(_on_Btn_update_preset)
+ Globals.change_canvas.connect(_on_change_canvas)
+
+
+ if stream:
+ httpsse_client = HTTPSSEClient.new()
+ add_child(httpsse_client)
+ httpsse_client.new_sse_event.connect(_on_new_sse_event)
+ else:
+ http_request = HTTPRequest.new()
+ add_child(http_request)
+ http_request.request_completed.connect(_on_request_completed)
+
+ system_message = {"role": "system", "content": "简短回复"}
+
+ load_preset()
+
+var old_node_name := "Canvas"
+func _on_change_canvas(path:String):
+ var CANVANS := load(path)
+ var new_canvas = CANVANS.instantiate()
+
+ var app_node = get_node("/root/App")
+
+ app_node.get_node(str(old_node_name)).free()
+ app_node.add_child(new_canvas)
+ canvas = new_canvas
+
+ chat_message_ai = get_node("/root/App/"+ new_canvas.name +"/Dialogue/PanelContainer/MarginContainer/ChatMessageAI")
+
+ old_node_name = str(new_canvas.name)
+
+func load_preset():
+ if !Globals.current_preset.is_empty():
+ preset_set(Globals.current_preset)
+
+
+
+func _unhandled_input(event: InputEvent) -> void:
+ if event.is_action_pressed("exit"):
+ get_tree().quit()
+ get_tree().root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST)
+
+
+func _on_Btn_send(content:Array):
+ if api_key == "" or model == "" or host == "":
+ $Send.title = "请先配置好“请求”参数"
+ return
+ $Send.title = "SendBox"
+ if stream:
+ chat_message_ai.clear()
+ chat_with_stream(content)
+ else:
+ chat_without_stream(content)
+
+func _on_Btn_update_preset(preset :Dictionary):
+ preset_set(preset)
+
+# 复制请求的参数
+func preset_set(preset :Dictionary):
+ host = preset["api"]['host']
+ path = preset['api']['path']
+ api_key = preset["api"]['key']
+ model = preset["api"]["model"]
+
+ stream = preset['parameters']['stream']
+ temperature = preset['parameters']['temperature']
+ top_p = preset['parameters']['topp']
+ history_count = preset['parameters']['historycount']
+ max_tokens = preset['parameters']['maxtokens']
+ system_message = {"role": "system", "content": preset['parameters']['systemprompt'] }
+
+ request_url = host + path
+ headers = ["Authorization: Bearer " + api_key, "Content-Type: application/json"]
+
+## 发送请求
+func chat_with_stream(content):
+ var messages = [system_message]
+ var last_N_messages = chat.slice(-history_count, chat.size(), 1)
+ messages.append_array(last_N_messages)
+
+ var new_message = {"role": "user", "content": content}
+ messages.append(new_message)
+
+ var request_body = JSON.stringify({
+ "messages": messages,
+ "stream": true,
+ "temperature": temperature,
+ "model": model,
+ "top_p": top_p
+ })
+ httpsse_client.connect_to_host(host, path, headers, request_body, chat_message_ai, 443)
+
+func chat_without_stream(content):
+ var messages = [system_message]
+ var last_N_messages = chat.slice(-history_count, chat.size(), 1)
+ messages.append_array(last_N_messages)
+
+ var new_message = {"role": "user", "content": content}
+ messages.append(new_message)
+
+ var request_body = JSON.stringify({
+ "messages": messages,
+ "temperature": temperature,
+ "model": model
+ })
+ var err := http_request.request(request_url, headers, HTTPClient.METHOD_POST, request_body)
+
+ if err != OK:
+ print("Error initiating HTTP request: ", err)
+
+## 返回请求
+func _on_request_completed(_result, _response_code, _headers, body):
+ var json = JSON.new()
+ var body_string = body.get_string_from_utf8()
+ var _Error = json.parse(body_string)
+ var response = json.get_data()
+ var message = response["choices"][0]["message"]["content"]
+ chat_msg_add(message)
+
+func _on_new_sse_event(partial_reply: Array, _ai_status_message: ChatMessageAI):
+ for msg in partial_reply:
+ if msg == '[DONE]':
+ # reply is over
+ httpsse_client.close_connection()
+ chat.append({"role": "assistant", "content":chat_message_ai.text})
+ Globals.is_busy = false
+# hide diague
+ canvas.start_hide_dialogue()
+ elif msg == "[EMPTY DELTA]":
+ continue
+ elif msg == "[ERROR]":
+ pass
+ else:
+ chat_msg_add(msg)
+
+func chat_msg_add(msg:String):
+ if stream:
+ chat_message_ai.add_text(msg)
+ else:
+ chat_message_ai.text = msg
+
+
+
diff --git a/app.tscn b/app.tscn
new file mode 100644
index 00000000..0f610dd9
--- /dev/null
+++ b/app.tscn
@@ -0,0 +1,14 @@
+[gd_scene load_steps=4 format=3 uid="uid://c2s5ftqtdj82m"]
+
+[ext_resource type="Script" path="res://app.gd" id="1_8qkjp"]
+[ext_resource type="PackedScene" uid="uid://dkmf1gpprqkdb" path="res://themes/Base_Canvas/canvas.tscn" id="2_g0u3y"]
+[ext_resource type="PackedScene" uid="uid://dsohs7cemnsss" path="res://send/send.tscn" id="6_h0gyl"]
+
+[node name="App" type="Node"]
+script = ExtResource("1_8qkjp")
+
+[node name="Send" parent="." instance=ExtResource("6_h0gyl")]
+position = Vector2i(756, 455)
+size = Vector2i(650, 400)
+
+[node name="Canvas" parent="." instance=ExtResource("2_g0u3y")]
diff --git a/build/windows/desktop_pet_godot.console.exe b/build/windows/desktop_pet_godot.console.exe
new file mode 100644
index 00000000..2e389039
Binary files /dev/null and b/build/windows/desktop_pet_godot.console.exe differ
diff --git a/build/windows/desktop_pet_godot.exe b/build/windows/desktop_pet_godot.exe
new file mode 100644
index 00000000..3f2776df
Binary files /dev/null and b/build/windows/desktop_pet_godot.exe differ
diff --git a/build/windows/desktop_pet_godot.pck b/build/windows/desktop_pet_godot.pck
new file mode 100644
index 00000000..2ed61cd8
Binary files /dev/null and b/build/windows/desktop_pet_godot.pck differ
diff --git a/classes/Globals.gd b/classes/Globals.gd
new file mode 100644
index 00000000..b3b39ccb
--- /dev/null
+++ b/classes/Globals.gd
@@ -0,0 +1,53 @@
+extends Node
+
+# 按下发送按钮时候,发送消息给gpt
+signal send_button_press(content :Array)
+
+# 更新请求接口
+signal update_request(presets :Dictionary)
+
+signal add_new_preset_button(preset :Dictionary)
+
+signal change_canvas(path :String)
+
+const SAVE_PATH := "user://presets.json"
+
+const CURRENT_PRESET_SAVE_PATH = "user://current_presets.json"
+
+# 预设
+var presets := {}
+
+var current_preset :Dictionary = {}
+
+# judge whether had sent request
+var is_busy = false
+
+# before start , load all presets to presets from loacl
+func _ready() -> void:
+ #get_tree().auto_accept_quit = true
+ current_preset = json_read(CURRENT_PRESET_SAVE_PATH)
+ presets = json_read(SAVE_PATH)
+
+func json_store_file(dic :Dictionary,path :String):
+ var json := JSON.stringify(dic)
+ var file := FileAccess.open(path,FileAccess.WRITE)
+ file.store_string(json)
+ file.close()
+
+func json_read(path:String) -> Dictionary:
+ var file = FileAccess.open(path,FileAccess.READ)
+ if file == null:
+ # not this file.json
+ return {}
+ var content := JSON.parse_string(file.get_as_text()) as Dictionary
+ file.close()
+ return content
+
+func _notification(what: int) -> void:
+ if what == NOTIFICATION_WM_CLOSE_REQUEST:
+ print('json_store_file')
+ json_store_file(current_preset,CURRENT_PRESET_SAVE_PATH)
+ json_store_file(presets,SAVE_PATH)
+ get_tree().quit()
+
+
diff --git a/export_presets.cfg b/export_presets.cfg
new file mode 100644
index 00000000..e51d1f64
--- /dev/null
+++ b/export_presets.cfg
@@ -0,0 +1,265 @@
+[preset.0]
+
+name="Android"
+platform="Android"
+runnable=true
+dedicated_server=false
+custom_features=""
+export_filter="all_resources"
+include_filter=""
+exclude_filter=""
+export_path=""
+encryption_include_filters=""
+encryption_exclude_filters=""
+encrypt_pck=false
+encrypt_directory=false
+
+[preset.0.options]
+
+custom_template/debug=""
+custom_template/release=""
+gradle_build/use_gradle_build=false
+gradle_build/export_format=0
+gradle_build/min_sdk=""
+gradle_build/target_sdk=""
+architectures/armeabi-v7a=false
+architectures/arm64-v8a=true
+architectures/x86=false
+architectures/x86_64=false
+version/code=1
+version/name=""
+package/unique_name="com.example.$genname"
+package/name=""
+package/signed=true
+package/app_category=2
+package/retain_data_on_uninstall=false
+package/exclude_from_recents=false
+package/show_in_android_tv=false
+package/show_in_app_library=true
+package/show_as_launcher_app=false
+launcher_icons/main_192x192=""
+launcher_icons/adaptive_foreground_432x432=""
+launcher_icons/adaptive_background_432x432=""
+graphics/opengl_debug=false
+xr_features/xr_mode=0
+screen/immersive_mode=true
+screen/support_small=true
+screen/support_normal=true
+screen/support_large=true
+screen/support_xlarge=true
+user_data_backup/allow=false
+command_line/extra_args=""
+apk_expansion/enable=false
+apk_expansion/SALT=""
+apk_expansion/public_key=""
+permissions/custom_permissions=PackedStringArray()
+permissions/access_checkin_properties=false
+permissions/access_coarse_location=false
+permissions/access_fine_location=false
+permissions/access_location_extra_commands=false
+permissions/access_mock_location=false
+permissions/access_network_state=false
+permissions/access_surface_flinger=false
+permissions/access_wifi_state=false
+permissions/account_manager=false
+permissions/add_voicemail=false
+permissions/authenticate_accounts=false
+permissions/battery_stats=false
+permissions/bind_accessibility_service=false
+permissions/bind_appwidget=false
+permissions/bind_device_admin=false
+permissions/bind_input_method=false
+permissions/bind_nfc_service=false
+permissions/bind_notification_listener_service=false
+permissions/bind_print_service=false
+permissions/bind_remoteviews=false
+permissions/bind_text_service=false
+permissions/bind_vpn_service=false
+permissions/bind_wallpaper=false
+permissions/bluetooth=false
+permissions/bluetooth_admin=false
+permissions/bluetooth_privileged=false
+permissions/brick=false
+permissions/broadcast_package_removed=false
+permissions/broadcast_sms=false
+permissions/broadcast_sticky=false
+permissions/broadcast_wap_push=false
+permissions/call_phone=false
+permissions/call_privileged=false
+permissions/camera=false
+permissions/capture_audio_output=false
+permissions/capture_secure_video_output=false
+permissions/capture_video_output=false
+permissions/change_component_enabled_state=false
+permissions/change_configuration=false
+permissions/change_network_state=false
+permissions/change_wifi_multicast_state=false
+permissions/change_wifi_state=false
+permissions/clear_app_cache=false
+permissions/clear_app_user_data=false
+permissions/control_location_updates=false
+permissions/delete_cache_files=false
+permissions/delete_packages=false
+permissions/device_power=false
+permissions/diagnostic=false
+permissions/disable_keyguard=false
+permissions/dump=false
+permissions/expand_status_bar=false
+permissions/factory_test=false
+permissions/flashlight=false
+permissions/force_back=false
+permissions/get_accounts=false
+permissions/get_package_size=false
+permissions/get_tasks=false
+permissions/get_top_activity_info=false
+permissions/global_search=false
+permissions/hardware_test=false
+permissions/inject_events=false
+permissions/install_location_provider=false
+permissions/install_packages=false
+permissions/install_shortcut=false
+permissions/internal_system_window=false
+permissions/internet=false
+permissions/kill_background_processes=false
+permissions/location_hardware=false
+permissions/manage_accounts=false
+permissions/manage_app_tokens=false
+permissions/manage_documents=false
+permissions/manage_external_storage=false
+permissions/master_clear=false
+permissions/media_content_control=false
+permissions/modify_audio_settings=false
+permissions/modify_phone_state=false
+permissions/mount_format_filesystems=false
+permissions/mount_unmount_filesystems=false
+permissions/nfc=false
+permissions/persistent_activity=false
+permissions/post_notifications=false
+permissions/process_outgoing_calls=false
+permissions/read_calendar=false
+permissions/read_call_log=false
+permissions/read_contacts=false
+permissions/read_external_storage=false
+permissions/read_frame_buffer=false
+permissions/read_history_bookmarks=false
+permissions/read_input_state=false
+permissions/read_logs=false
+permissions/read_phone_state=false
+permissions/read_profile=false
+permissions/read_sms=false
+permissions/read_social_stream=false
+permissions/read_sync_settings=false
+permissions/read_sync_stats=false
+permissions/read_user_dictionary=false
+permissions/reboot=false
+permissions/receive_boot_completed=false
+permissions/receive_mms=false
+permissions/receive_sms=false
+permissions/receive_wap_push=false
+permissions/record_audio=false
+permissions/reorder_tasks=false
+permissions/restart_packages=false
+permissions/send_respond_via_message=false
+permissions/send_sms=false
+permissions/set_activity_watcher=false
+permissions/set_alarm=false
+permissions/set_always_finish=false
+permissions/set_animation_scale=false
+permissions/set_debug_app=false
+permissions/set_orientation=false
+permissions/set_pointer_speed=false
+permissions/set_preferred_applications=false
+permissions/set_process_limit=false
+permissions/set_time=false
+permissions/set_time_zone=false
+permissions/set_wallpaper=false
+permissions/set_wallpaper_hints=false
+permissions/signal_persistent_processes=false
+permissions/status_bar=false
+permissions/subscribed_feeds_read=false
+permissions/subscribed_feeds_write=false
+permissions/system_alert_window=false
+permissions/transmit_ir=false
+permissions/uninstall_shortcut=false
+permissions/update_device_stats=false
+permissions/use_credentials=false
+permissions/use_sip=false
+permissions/vibrate=false
+permissions/wake_lock=false
+permissions/write_apn_settings=false
+permissions/write_calendar=false
+permissions/write_call_log=false
+permissions/write_contacts=false
+permissions/write_external_storage=false
+permissions/write_gservices=false
+permissions/write_history_bookmarks=false
+permissions/write_profile=false
+permissions/write_secure_settings=false
+permissions/write_settings=false
+permissions/write_sms=false
+permissions/write_social_stream=false
+permissions/write_sync_settings=false
+permissions/write_user_dictionary=false
+
+[preset.1]
+
+name="Windows Desktop"
+platform="Windows Desktop"
+runnable=true
+dedicated_server=false
+custom_features=""
+export_filter="all_resources"
+include_filter=""
+exclude_filter=""
+export_path="build/windows/desktop_pet_godot.exe"
+encryption_include_filters=""
+encryption_exclude_filters=""
+encrypt_pck=false
+encrypt_directory=false
+
+[preset.1.options]
+
+custom_template/debug=""
+custom_template/release=""
+debug/export_console_wrapper=1
+binary_format/embed_pck=false
+texture_format/bptc=true
+texture_format/s3tc=true
+texture_format/etc=false
+texture_format/etc2=false
+binary_format/architecture="x86_64"
+codesign/enable=false
+codesign/timestamp=true
+codesign/timestamp_server_url=""
+codesign/digest_algorithm=1
+codesign/description=""
+codesign/custom_options=PackedStringArray()
+application/modify_resources=true
+application/icon=""
+application/console_wrapper_icon=""
+application/icon_interpolation=4
+application/file_version=""
+application/product_version=""
+application/company_name=""
+application/product_name=""
+application/file_description=""
+application/copyright=""
+application/trademarks=""
+application/export_angle=0
+ssh_remote_deploy/enabled=false
+ssh_remote_deploy/host="user@host_ip"
+ssh_remote_deploy/port="22"
+ssh_remote_deploy/extra_args_ssh=""
+ssh_remote_deploy/extra_args_scp=""
+ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
+$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
+$trigger = New-ScheduledTaskTrigger -Once -At 00:00
+$settings = New-ScheduledTaskSettingsSet
+$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
+Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
+Start-ScheduledTask -TaskName godot_remote_debug
+while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
+Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
+ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
+Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
+Remove-Item -Recurse -Force '{temp_dir}'"
diff --git a/icon.svg b/icon.svg
new file mode 100644
index 00000000..b370ceb7
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1 @@
+
diff --git a/icon.svg.import b/icon.svg.import
new file mode 100644
index 00000000..b40d57d0
--- /dev/null
+++ b/icon.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cou3j3fpt3c5v"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/img/Warrior_Blue.png b/img/Warrior_Blue.png
new file mode 100644
index 00000000..a40e9c20
Binary files /dev/null and b/img/Warrior_Blue.png differ
diff --git a/img/Warrior_Blue.png.import b/img/Warrior_Blue.png.import
new file mode 100644
index 00000000..3b7f5c73
--- /dev/null
+++ b/img/Warrior_Blue.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://duyugi5bh5yy1"
+path="res://.godot/imported/Warrior_Blue.png-10a99b2c93cbc047c02f6a2038fefcf1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://img/Warrior_Blue.png"
+dest_files=["res://.godot/imported/Warrior_Blue.png-10a99b2c93cbc047c02f6a2038fefcf1.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/img/example.png b/img/example.png
new file mode 100644
index 00000000..384cfc05
Binary files /dev/null and b/img/example.png differ
diff --git a/img/example.png.import b/img/example.png.import
new file mode 100644
index 00000000..57d1642a
--- /dev/null
+++ b/img/example.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bwx3m3jwg4ome"
+path="res://.godot/imported/example.png-ca19a06c7cff21c4e05c173364314286.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://img/example.png"
+dest_files=["res://.godot/imported/example.png-ca19a06c7cff21c4e05c173364314286.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/img/exmaple.png.import b/img/exmaple.png.import
new file mode 100644
index 00000000..ee12bb58
--- /dev/null
+++ b/img/exmaple.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dkq1r2d34a51s"
+path="res://.godot/imported/exmaple.png-9371d64d8e54e0be42007139f4f9a7bf.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://img/exmaple.png"
+dest_files=["res://.godot/imported/exmaple.png-9371d64d8e54e0be42007139f4f9a7bf.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/project.godot b/project.godot
new file mode 100644
index 00000000..b804deda
--- /dev/null
+++ b/project.godot
@@ -0,0 +1,54 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="Desktop-Pet-Godot"
+config/description="🚀 🚀 🚀 一款由大语言模型驱动、Godot 制作的 AI 桌宠,旨在提供一个全能的、丰富的桌面AI宠物。"
+run/main_scene="res://app.tscn"
+config/features=PackedStringArray("4.2", "Forward Plus")
+run/low_processor_mode=true
+boot_splash/bg_color=Color(0.141176, 0.141176, 0.141176, 0)
+boot_splash/image="res://themes/Base_Canvas/assets/UI/transparent.png"
+config/icon="res://icon.svg"
+
+[autoload]
+
+Globals="*res://classes/Globals.gd"
+
+[display]
+
+window/size/viewport_width=1
+window/size/viewport_height=1
+window/size/borderless=true
+window/size/always_on_top=true
+window/size/transparent=true
+window/subwindows/embed_subwindows=false
+window/per_pixel_transparency/allowed=true
+
+[editor_plugins]
+
+enabled=PackedStringArray("res://addons/HTTPSSEClient/plugin.cfg")
+
+[input]
+
+exit={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"echo":false,"script":null)
+]
+}
+
+[rendering]
+
+textures/canvas_textures/default_texture_filter=0
+renderer/rendering_method="gl_compatibility"
+renderer/rendering_method.mobile="gl_compatibility"
+environment/defaults/default_clear_color=Color(0, 0, 0, 1)
+viewport/transparent_background=true
diff --git a/send/Send.gd b/send/Send.gd
new file mode 100644
index 00000000..636d4dc4
--- /dev/null
+++ b/send/Send.gd
@@ -0,0 +1,5 @@
+extends Window
+
+# 关闭发送窗口
+func _on_close_requested() -> void:
+ queue_free()
diff --git a/send/chat_box/chat_msg_edit.gd b/send/chat_box/chat_msg_edit.gd
new file mode 100644
index 00000000..f97945c6
--- /dev/null
+++ b/send/chat_box/chat_msg_edit.gd
@@ -0,0 +1,76 @@
+extends TextEdit
+
+var content : Array[Dictionary] = [
+ #{
+ #"type": "text",
+ #"text": ""
+ #},
+ #{
+ #"type": "image_url",
+ #"image_url": {
+ #"url": "data:image/png;base64,{base64_image}"
+ #}
+ #}
+]
+
+var base64_image : String = ""
+
+func _gui_input(event: InputEvent) -> void:
+ if event is InputEventKey:
+ if event.pressed and event.keycode == KEY_ENTER:
+ if event.shift_pressed:
+ insert_text_at_caret("\n")
+ else:
+ if not Globals.is_busy:
+ _send_content()
+
+func _send_content():
+ var text_section : Dictionary = {
+ "type": "text",
+ "text": text
+ }
+ content.append(text_section)
+
+ if base64_image != "":
+ var image_section : Dictionary = {
+ "type": "image_url",
+ "image_url": {
+ "url": "data:image/png;base64,{%s}" % [base64_image]
+ }
+ }
+ base64_image = ""
+ content.append(image_section)
+
+ Globals.send_button_press.emit(content)
+ Globals.is_busy = true
+
+ content.clear()
+ await get_tree().create_timer(0.2).timeout
+ text = ""
+ print("prompt text = ",text)
+ texture_queue_free()
+
+func _on_button_pressed() -> void:
+ if not Globals.is_busy:
+ _send_content()
+
+func _paste(_caret_index: int) -> void:
+ if DisplayServer.clipboard_has_image():
+ var image : Image = DisplayServer.clipboard_get_image()
+ #$"../Sprite2D".texture = ImageTexture.create_from_image(image)
+ base64_image = Marshalls.raw_to_base64(image.save_png_to_buffer())
+ #print("data:image/png;base64,{%s}" % [base64_image])
+ print("image add")
+ add_texture(image)
+
+func add_texture(image:Image):
+ var texture_rect : TextureRect = TextureRect.new()
+ texture_rect.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
+ texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ texture_rect.texture = ImageTexture.create_from_image(image)
+ %ToolBar.add_child(texture_rect)
+
+func texture_queue_free():
+ for child in %ToolBar.get_children():
+ child.queue_free()
+
diff --git a/send/edit_preset/preset.gd b/send/edit_preset/preset.gd
new file mode 100644
index 00000000..c1c3b871
--- /dev/null
+++ b/send/edit_preset/preset.gd
@@ -0,0 +1,65 @@
+extends Control
+
+@onready var model_grid: GridContainer = %ModelGrid
+@onready var parament_grid: GridContainer = %ParamentGrid
+@onready var type: OptionButton = $MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid/Type
+@onready var stream: CheckButton = $MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid/Stream
+
+var preset : Dictionary = {
+ "name": "robot",
+ "api": {
+ "type": "OpenAI",
+ "host": "https://api.openai.com",
+ "path": "/v1/chat/completions",
+ "key": "",
+ "model": "gpt-4o"
+ },
+ "parameters": {
+ "stream": true,
+ "temperature": 1.0,
+ "topp": 0.3,
+ "historycount": 5,
+ "maxtokens": 3000,
+ "systemprompt": ""
+ },
+}
+
+func _ready() -> void:
+ stream.button_pressed = true
+
+func store_preset_dic():
+ for child in model_grid.get_children():
+ if child is LineEdit:
+ if child.name.to_lower() != "name":
+ preset["api"][child.name.to_lower()] = child.text
+
+ for child in parament_grid.get_children():
+ if child is TextEdit:
+ preset["parameters"][child.name.to_lower()] = child.text
+
+ if child is LineEdit:
+ var little_name := child.name.to_lower()
+ if little_name == "stream":
+ continue
+ match little_name:
+ "historycount": preset["parameters"][little_name] = child.text.to_int()
+ "topp": preset["parameters"][little_name] = child.text.to_float()
+ "temperature": preset["parameters"][little_name] = child.text.to_float()
+ "maxtokens" : preset["parameters"][little_name] = child.text.to_int()
+
+
+func _on_type_item_selected(index: int) -> void:
+ preset["api"]["type"] = type.get_item_text(index)
+
+func _on_name_text_changed(new_text: String) -> void:
+ preset["name"] = new_text
+
+func _on_stream_pressed() -> void:
+ preset["parameters"]["stream"] = stream.button_pressed
+
+func _on_button_pressed() -> void:
+ store_preset_dic()
+ print(preset)
+ # modify the exist preset or add new preset
+ Globals.presets[preset["name"]] = preset
+ Globals.add_new_preset_button.emit(preset)
diff --git a/send/edit_preset/preset.tscn b/send/edit_preset/preset.tscn
new file mode 100644
index 00000000..6cf6c966
--- /dev/null
+++ b/send/edit_preset/preset.tscn
@@ -0,0 +1,246 @@
+[gd_scene load_steps=3 format=3 uid="uid://h2ghtx4klqip"]
+
+[ext_resource type="Script" path="res://send/edit_preset/preset.gd" id="1_8tp2p"]
+[ext_resource type="Theme" uid="uid://7wp65kjcrv2s" path="res://themes/Base_Canvas/main.tres" id="2_nacpe"]
+
+[node name="编辑预设" type="Control"]
+visible = false
+layout_mode = 3
+anchors_preset = 0
+size_flags_horizontal = 3
+size_flags_vertical = 3
+script = ExtResource("1_8tp2p")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_top = 31.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 10
+theme_override_constants/margin_top = 5
+theme_override_constants/margin_right = 10
+theme_override_constants/margin_bottom = 5
+
+[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer"]
+layout_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Model" type="PanelContainer" parent="MarginContainer/ScrollContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/ScrollContainer/VBoxContainer/Model"]
+layout_mode = 2
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_right = 20
+
+[node name="ModelGrid" type="GridContainer" parent="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme = ExtResource("2_nacpe")
+columns = 2
+
+[node name="NameLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid"]
+layout_mode = 2
+text = "名称"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Name" type="LineEdit" parent="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid"]
+layout_mode = 2
+
+[node name="TypeLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "API形式"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Type" type="OptionButton" parent="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+item_count = 4
+selected = 0
+popup/item_0/text = "OpenAI"
+popup/item_0/id = 4
+popup/item_1/text = "DeepSeek"
+popup/item_1/id = 0
+popup/item_2/text = "Qwen"
+popup/item_2/id = 1
+popup/item_3/text = "Yi"
+popup/item_3/id = 2
+
+[node name="HostLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "host"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Host" type="LineEdit" parent="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "https://api.openai.com"
+
+[node name="PathLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "Path"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Path" type="LineEdit" parent="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "/v1/chat/completions"
+
+[node name="KeyLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "API Key"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Key" type="LineEdit" parent="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="ModelLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "模型名称"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Model" type="LineEdit" parent="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "gpt-4o"
+
+[node name="Parament" type="PanelContainer" parent="MarginContainer/ScrollContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament"]
+layout_mode = 2
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_right = 20
+
+[node name="ParamentGrid" type="GridContainer" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+columns = 2
+
+[node name="StreamLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid"]
+visible = false
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "stream流式输出"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Stream" type="CheckButton" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid"]
+visible = false
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+button_pressed = true
+
+[node name="TemperatureLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "temperature"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Temperature" type="LineEdit" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "1.0"
+
+[node name="TopPLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "top_p"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="TopP" type="LineEdit" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "0.3"
+
+[node name="HistoryCountLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "附带历史消息条数"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="HistoryCount" type="LineEdit" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "5"
+
+[node name="MaxTokensLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "max_tokens"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="MaxTokens" type="LineEdit" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+text = "3000"
+
+[node name="SystemPromptLabel" type="Label" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid"]
+layout_mode = 2
+size_flags_vertical = 3
+text = "系统提示词"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="SystemPrompt" type="TextEdit" parent="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid"]
+layout_mode = 2
+size_flags_vertical = 3
+text = "you are a helpful helper"
+
+[node name="Button" type="Button" parent="MarginContainer/ScrollContainer/VBoxContainer"]
+layout_mode = 2
+text = "保存(修改)"
+
+[connection signal="text_changed" from="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid/Name" to="." method="_on_name_text_changed"]
+[connection signal="item_selected" from="MarginContainer/ScrollContainer/VBoxContainer/Model/MarginContainer/ModelGrid/Type" to="." method="_on_type_item_selected"]
+[connection signal="pressed" from="MarginContainer/ScrollContainer/VBoxContainer/Parament/MarginContainer/ParamentGrid/Stream" to="." method="_on_stream_pressed"]
+[connection signal="pressed" from="MarginContainer/ScrollContainer/VBoxContainer/Button" to="." method="_on_button_pressed"]
diff --git a/send/send.tscn b/send/send.tscn
new file mode 100644
index 00000000..b62c11bb
--- /dev/null
+++ b/send/send.tscn
@@ -0,0 +1,113 @@
+[gd_scene load_steps=6 format=3 uid="uid://dsohs7cemnsss"]
+
+[ext_resource type="Script" path="res://send/send.gd" id="1_875b0"]
+[ext_resource type="Script" path="res://send/chat_box/chat_msg_edit.gd" id="2_l2itr"]
+[ext_resource type="Script" path="res://send/store_preset/store_preset.gd" id="3_hthny"]
+[ext_resource type="PackedScene" uid="uid://h2ghtx4klqip" path="res://send/edit_preset/preset.tscn" id="5_0uube"]
+[ext_resource type="PackedScene" uid="uid://c3qe413dfh71i" path="res://send/themes/themes.tscn" id="5_1e2g1"]
+
+[node name="Send" type="Window"]
+title = "SendBox"
+position = Vector2i(806, 303)
+size = Vector2i(600, 500)
+always_on_top = true
+script = ExtResource("1_875b0")
+
+[node name="TabContainer" type="TabContainer" parent="."]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+current_tab = 3
+
+[node name="对话栏" type="Control" parent="TabContainer"]
+visible = false
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="MarginContainer" type="MarginContainer" parent="TabContainer/对话栏"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 2
+theme_override_constants/margin_top = 2
+theme_override_constants/margin_right = 2
+theme_override_constants/margin_bottom = 2
+
+[node name="PanelContainer" type="PanelContainer" parent="TabContainer/对话栏/MarginContainer"]
+layout_mode = 2
+
+[node name="BoxContainer" type="VBoxContainer" parent="TabContainer/对话栏/MarginContainer/PanelContainer"]
+layout_mode = 2
+
+[node name="ToolBar" type="HBoxContainer" parent="TabContainer/对话栏/MarginContainer/PanelContainer/BoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="SendBar" type="HBoxContainer" parent="TabContainer/对话栏/MarginContainer/PanelContainer/BoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+size_flags_stretch_ratio = 3.0
+
+[node name="ChatMsgEdit" type="TextEdit" parent="TabContainer/对话栏/MarginContainer/PanelContainer/BoxContainer/SendBar"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(20, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 5.0
+script = ExtResource("2_l2itr")
+
+[node name="Button" type="Button" parent="TabContainer/对话栏/MarginContainer/PanelContainer/BoxContainer/SendBar"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Send"
+
+[node name="存储预设" type="Control" parent="TabContainer"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+script = ExtResource("3_hthny")
+
+[node name="PanelContainer" type="PanelContainer" parent="TabContainer/存储预设"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="TabContainer/存储预设/PanelContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 5
+theme_override_constants/margin_top = 5
+theme_override_constants/margin_right = 5
+theme_override_constants/margin_bottom = 5
+
+[node name="ScrollContainer" type="ScrollContainer" parent="TabContainer/存储预设/PanelContainer/MarginContainer"]
+layout_mode = 2
+
+[node name="PresetsContainer" type="GridContainer" parent="TabContainer/存储预设/PanelContainer/MarginContainer/ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/h_separation = 5
+theme_override_constants/v_separation = 5
+columns = 4
+
+[node name="编辑预设" parent="TabContainer" instance=ExtResource("5_0uube")]
+layout_mode = 2
+
+[node name="主题" parent="TabContainer" instance=ExtResource("5_1e2g1")]
+layout_mode = 2
+
+[connection signal="close_requested" from="." to="." method="_on_close_requested"]
+[connection signal="pressed" from="TabContainer/对话栏/MarginContainer/PanelContainer/BoxContainer/SendBar/Button" to="TabContainer/对话栏/MarginContainer/PanelContainer/BoxContainer/SendBar/ChatMsgEdit" method="_on_button_pressed"]
diff --git a/send/store_preset/store_preset.gd b/send/store_preset/store_preset.gd
new file mode 100644
index 00000000..e3feef7e
--- /dev/null
+++ b/send/store_preset/store_preset.gd
@@ -0,0 +1,21 @@
+extends Control
+
+@onready var presets_container: GridContainer = %PresetsContainer
+
+func _ready() -> void:
+ Globals.add_new_preset_button.connect(_change_type_load_presets)
+ _load_presets(Globals.presets)
+
+func _change_type_load_presets(preset :Dictionary):
+ _load_presets({preset['name']:preset})
+
+func _load_presets(presets :Dictionary):
+ for preset in presets.values():
+ var button := Button.new()
+ button.text = preset['name']
+ button.pressed.connect(_select_presets.bind(preset))
+ presets_container.add_child(button)
+
+func _select_presets(preset :Dictionary):
+ Globals.current_preset = preset
+ Globals.update_request.emit(preset)
diff --git a/send/themes/themes.gd b/send/themes/themes.gd
new file mode 100644
index 00000000..f9eec19c
--- /dev/null
+++ b/send/themes/themes.gd
@@ -0,0 +1,27 @@
+extends Control
+
+
+@onready var grid_container: GridContainer = %GridContainer
+
+var themes_scenes :Array[String] = [
+ "res://themes/Tiny_Swords/tiny_swords_canvas.tscn",
+ "res://themes/Base_Canvas/canvas.tscn"
+]
+
+
+func _ready() -> void:
+ _change_theme()
+
+
+func _change_theme():
+
+ for i in themes_scenes:
+ var button := Button.new()
+ var arr := i.split('/')
+ button.text = arr[arr.size()-1].left(-5)
+ button.pressed.connect(_select_presets.bind(i))
+ grid_container.add_child(button)
+
+func _select_presets(path :String):
+ print(path)
+ Globals.change_canvas.emit(path)
diff --git a/send/themes/themes.tscn b/send/themes/themes.tscn
new file mode 100644
index 00000000..8d0213f6
--- /dev/null
+++ b/send/themes/themes.tscn
@@ -0,0 +1,28 @@
+[gd_scene load_steps=2 format=3 uid="uid://c3qe413dfh71i"]
+
+[ext_resource type="Script" path="res://send/themes/themes.gd" id="1_5dral"]
+
+[node name="主题" type="Control"]
+layout_mode = 3
+anchors_preset = 0
+script = ExtResource("1_5dral")
+
+[node name="PanelContainer" type="PanelContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
+layout_mode = 2
+
+[node name="ScrollContainer" type="ScrollContainer" parent="PanelContainer/MarginContainer"]
+layout_mode = 2
+
+[node name="GridContainer" type="GridContainer" parent="PanelContainer/MarginContainer/ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
diff --git a/test/test.gd b/test/test.gd
new file mode 100644
index 00000000..0e2c1e7f
--- /dev/null
+++ b/test/test.gd
@@ -0,0 +1,10 @@
+extends Node
+
+var dic := {}
+
+
+func _ready() -> void:
+ dic['你好'] = 1
+ print(dic)
+ dic['他该'] = 2
+ print(dic)
diff --git a/test/test.tscn b/test/test.tscn
new file mode 100644
index 00000000..9d4688bb
--- /dev/null
+++ b/test/test.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://c72cr8byw6apu"]
+
+[ext_resource type="Script" path="res://test/test.gd" id="1_gwm6m"]
+
+[node name="test" type="Node"]
+script = ExtResource("1_gwm6m")
diff --git a/themes/Base_Canvas/assets/UI/transparent.png b/themes/Base_Canvas/assets/UI/transparent.png
new file mode 100644
index 00000000..9fb90424
Binary files /dev/null and b/themes/Base_Canvas/assets/UI/transparent.png differ
diff --git a/themes/Base_Canvas/assets/UI/transparent.png.import b/themes/Base_Canvas/assets/UI/transparent.png.import
new file mode 100644
index 00000000..37395e96
--- /dev/null
+++ b/themes/Base_Canvas/assets/UI/transparent.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dr56habqc6luy"
+path="res://.godot/imported/transparent.png-5964354e41a4505fc07e8f893f7241b0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://themes/Base_Canvas/assets/UI/transparent.png"
+dest_files=["res://.godot/imported/transparent.png-5964354e41a4505fc07e8f893f7241b0.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/themes/Base_Canvas/assets/UI/ui x3.png b/themes/Base_Canvas/assets/UI/ui x3.png
new file mode 100644
index 00000000..7e22b95c
Binary files /dev/null and b/themes/Base_Canvas/assets/UI/ui x3.png differ
diff --git a/themes/Base_Canvas/assets/UI/ui x3.png.import b/themes/Base_Canvas/assets/UI/ui x3.png.import
new file mode 100644
index 00000000..3a8121c6
--- /dev/null
+++ b/themes/Base_Canvas/assets/UI/ui x3.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://qqko8bggda1n"
+path="res://.godot/imported/ui x3.png-900857131c350d516cfe304bee790169.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://themes/Base_Canvas/assets/UI/ui x3.png"
+dest_files=["res://.godot/imported/ui x3.png-900857131c350d516cfe304bee790169.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/themes/Base_Canvas/assets/characters/players grey x3.png b/themes/Base_Canvas/assets/characters/players grey x3.png
new file mode 100644
index 00000000..129d16de
Binary files /dev/null and b/themes/Base_Canvas/assets/characters/players grey x3.png differ
diff --git a/themes/Base_Canvas/assets/characters/players grey x3.png.import b/themes/Base_Canvas/assets/characters/players grey x3.png.import
new file mode 100644
index 00000000..ec91b47c
--- /dev/null
+++ b/themes/Base_Canvas/assets/characters/players grey x3.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://5m8pylbtdb68"
+path="res://.godot/imported/players grey x3.png-3d0b6eacec035a3edf719061425850fa.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://themes/Base_Canvas/assets/characters/players grey x3.png"
+dest_files=["res://.godot/imported/players grey x3.png-3d0b6eacec035a3edf719061425850fa.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/themes/Base_Canvas/assets/dialogue/dialgue_box1.png b/themes/Base_Canvas/assets/dialogue/dialgue_box1.png
new file mode 100644
index 00000000..0d851f1a
Binary files /dev/null and b/themes/Base_Canvas/assets/dialogue/dialgue_box1.png differ
diff --git a/themes/Base_Canvas/assets/dialogue/dialgue_box1.png.import b/themes/Base_Canvas/assets/dialogue/dialgue_box1.png.import
new file mode 100644
index 00000000..d79f9e3b
--- /dev/null
+++ b/themes/Base_Canvas/assets/dialogue/dialgue_box1.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c36m0gk0xqi3o"
+path="res://.godot/imported/dialgue_box1.png-73e82f1850be0e7279a98f9066cf1ba2.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://themes/Base_Canvas/assets/dialogue/dialgue_box1.png"
+dest_files=["res://.godot/imported/dialgue_box1.png-73e82f1850be0e7279a98f9066cf1ba2.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/themes/Base_Canvas/assets/fonts/LXGWNeoXiHei.ttf b/themes/Base_Canvas/assets/fonts/LXGWNeoXiHei.ttf
new file mode 100644
index 00000000..db9ed7e1
Binary files /dev/null and b/themes/Base_Canvas/assets/fonts/LXGWNeoXiHei.ttf differ
diff --git a/themes/Base_Canvas/assets/fonts/LXGWNeoXiHei.ttf.import b/themes/Base_Canvas/assets/fonts/LXGWNeoXiHei.ttf.import
new file mode 100644
index 00000000..08f57e4e
--- /dev/null
+++ b/themes/Base_Canvas/assets/fonts/LXGWNeoXiHei.ttf.import
@@ -0,0 +1,33 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://bytigbjqllywe"
+path="res://.godot/imported/LXGWNeoXiHei.ttf-50e88f51ed75df471439ecdcffa5ca99.fontdata"
+
+[deps]
+
+source_file="res://themes/Base_Canvas/assets/fonts/LXGWNeoXiHei.ttf"
+dest_files=["res://.godot/imported/LXGWNeoXiHei.ttf-50e88f51ed75df471439ecdcffa5ca99.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=1
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/themes/Base_Canvas/assets/fonts/SmileySans-Oblique.ttf b/themes/Base_Canvas/assets/fonts/SmileySans-Oblique.ttf
new file mode 100644
index 00000000..c297dc69
Binary files /dev/null and b/themes/Base_Canvas/assets/fonts/SmileySans-Oblique.ttf differ
diff --git a/themes/Base_Canvas/assets/fonts/SmileySans-Oblique.ttf.import b/themes/Base_Canvas/assets/fonts/SmileySans-Oblique.ttf.import
new file mode 100644
index 00000000..fd676f3c
--- /dev/null
+++ b/themes/Base_Canvas/assets/fonts/SmileySans-Oblique.ttf.import
@@ -0,0 +1,33 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://7ydt53m2btct"
+path="res://.godot/imported/SmileySans-Oblique.ttf-90f5cf1d5f4301e882afa9b7b3390de7.fontdata"
+
+[deps]
+
+source_file="res://themes/Base_Canvas/assets/fonts/SmileySans-Oblique.ttf"
+dest_files=["res://.godot/imported/SmileySans-Oblique.ttf-90f5cf1d5f4301e882afa9b7b3390de7.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=1
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/themes/Base_Canvas/assets/fonts/SourceHanSansSC-Normal.otf b/themes/Base_Canvas/assets/fonts/SourceHanSansSC-Normal.otf
new file mode 100644
index 00000000..3cb93369
Binary files /dev/null and b/themes/Base_Canvas/assets/fonts/SourceHanSansSC-Normal.otf differ
diff --git a/themes/Base_Canvas/assets/fonts/SourceHanSansSC-Normal.otf.import b/themes/Base_Canvas/assets/fonts/SourceHanSansSC-Normal.otf.import
new file mode 100644
index 00000000..91a18439
--- /dev/null
+++ b/themes/Base_Canvas/assets/fonts/SourceHanSansSC-Normal.otf.import
@@ -0,0 +1,33 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://dp8cmm8sp60uy"
+path="res://.godot/imported/SourceHanSansSC-Normal.otf-19b222bed4e3c810a40738b1e303883e.fontdata"
+
+[deps]
+
+source_file="res://themes/Base_Canvas/assets/fonts/SourceHanSansSC-Normal.otf"
+dest_files=["res://.godot/imported/SourceHanSansSC-Normal.otf-19b222bed4e3c810a40738b1e303883e.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=1
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/themes/Base_Canvas/base_canvas.md b/themes/Base_Canvas/base_canvas.md
new file mode 100644
index 00000000..e69de29b
diff --git a/themes/Base_Canvas/canvas.gd b/themes/Base_Canvas/canvas.gd
new file mode 100644
index 00000000..ee8eed48
--- /dev/null
+++ b/themes/Base_Canvas/canvas.gd
@@ -0,0 +1,28 @@
+extends Node2D
+
+@onready var dialogue_timer: Timer = $Dialogue_Timer
+@onready var dialogue_node: Control = %Dialogue
+
+
+func _ready() -> void:
+ _initialize_window()
+ dialogue_timer.timeout.connect(_on_dialogue_timer)
+
+func _initialize_window() -> void:
+ var window: Window = get_window()
+ window.size = Vector2i(DisplayServer.screen_get_size() + Vector2i(1, 1))
+ window.position = DisplayServer.screen_get_position()
+
+
+func set_dialogue_label(msg:String):
+ %Dialogue.set_dialogue_label(msg)
+
+
+# 20s内没有任何操作,自动隐藏对话节点 ; 有操作就重新记时
+func start_hide_dialogue():
+ dialogue_node.show()
+
+ dialogue_timer.start(20)
+
+func _on_dialogue_timer():
+ dialogue_node.hide()
diff --git a/themes/Base_Canvas/canvas.tscn b/themes/Base_Canvas/canvas.tscn
new file mode 100644
index 00000000..d33f69c8
--- /dev/null
+++ b/themes/Base_Canvas/canvas.tscn
@@ -0,0 +1,120 @@
+[gd_scene load_steps=12 format=3 uid="uid://dkmf1gpprqkdb"]
+
+[ext_resource type="Script" path="res://themes/Base_Canvas/canvas.gd" id="1_s7786"]
+[ext_resource type="Script" path="res://themes/Base_Canvas/grapic/grapic.gd" id="2_a65q2"]
+[ext_resource type="Texture2D" uid="uid://5m8pylbtdb68" path="res://themes/Base_Canvas/assets/characters/players grey x3.png" id="3_s54au"]
+[ext_resource type="Theme" uid="uid://7wp65kjcrv2s" path="res://themes/Base_Canvas/main.tres" id="4_j53t2"]
+[ext_resource type="Script" path="res://themes/Base_Canvas/dialogue/dialogue.gd" id="4_lp7mw"]
+[ext_resource type="Texture2D" uid="uid://c36m0gk0xqi3o" path="res://themes/Base_Canvas/assets/dialogue/dialgue_box1.png" id="6_be7al"]
+[ext_resource type="Script" path="res://themes/Base_Canvas/dialogue/chat_message_ai.gd" id="7_4g7g5"]
+
+[sub_resource type="Animation" id="Animation_fqk1a"]
+length = 0.001
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath(".:frame")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 1,
+"values": [73]
+}
+
+[sub_resource type="Animation" id="Animation_8lixs"]
+resource_name = "idle"
+loop_mode = 1
+step = 0.2
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath(".:frame")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 0.2, 0.4, 0.6, 0.8),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
+"update": 1,
+"values": [72, 73, 74, 75, 76]
+}
+
+[sub_resource type="AnimationLibrary" id="AnimationLibrary_tibt3"]
+_data = {
+"RESET": SubResource("Animation_fqk1a"),
+"idle": SubResource("Animation_8lixs")
+}
+
+[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_o4c6w"]
+texture = ExtResource("6_be7al")
+
+[node name="Canvas" type="Node2D"]
+position = Vector2(2373, 152)
+script = ExtResource("1_s7786")
+metadata/_edit_lock_ = true
+metadata/_edit_group_ = true
+
+[node name="Grapic" type="Node2D" parent="."]
+scale = Vector2(-2, 2)
+script = ExtResource("2_a65q2")
+
+[node name="Pet" type="Sprite2D" parent="Grapic"]
+position = Vector2(-5, 8)
+texture = ExtResource("3_s54au")
+hframes = 8
+vframes = 13
+frame = 73
+
+[node name="AnimationPlayer" type="AnimationPlayer" parent="Grapic/Pet"]
+libraries = {
+"": SubResource("AnimationLibrary_tibt3")
+}
+autoplay = "idle"
+
+[node name="Area2D" type="Area2D" parent="Grapic"]
+position = Vector2(2, 9)
+
+[node name="ClickPolygon" type="CollisionPolygon2D" parent="Grapic/Area2D"]
+unique_name_in_owner = true
+self_modulate = Color(1, 1, 1, 0.105882)
+polygon = PackedVector2Array(274, 64, -64, 64, -64, -64, 275, -64)
+disabled = true
+
+[node name="Dialogue_Timer" type="Timer" parent="."]
+wait_time = 20.0
+one_shot = true
+autostart = true
+
+[node name="Dialogue" type="Control" parent="."]
+unique_name_in_owner = true
+layout_mode = 3
+anchors_preset = 0
+offset_left = -541.0
+offset_top = -149.0
+offset_right = -73.0
+offset_bottom = 158.0
+theme = ExtResource("4_j53t2")
+script = ExtResource("4_lp7mw")
+
+[node name="PanelContainer" type="PanelContainer" parent="Dialogue"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_styles/panel = SubResource("StyleBoxTexture_o4c6w")
+
+[node name="MarginContainer" type="MarginContainer" parent="Dialogue/PanelContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 30
+theme_override_constants/margin_top = 120
+theme_override_constants/margin_right = 30
+theme_override_constants/margin_bottom = 40
+
+[node name="ChatMessageAI" type="RichTextLabel" parent="Dialogue/PanelContainer/MarginContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+bbcode_enabled = true
+script = ExtResource("7_4g7g5")
diff --git a/themes/Base_Canvas/dialogue/chat_message_ai.gd b/themes/Base_Canvas/dialogue/chat_message_ai.gd
new file mode 100644
index 00000000..df5a9323
--- /dev/null
+++ b/themes/Base_Canvas/dialogue/chat_message_ai.gd
@@ -0,0 +1,4 @@
+class_name ChatMessageAI
+extends RichTextLabel
+
+
diff --git a/themes/Base_Canvas/dialogue/dialogue.gd b/themes/Base_Canvas/dialogue/dialogue.gd
new file mode 100644
index 00000000..05693ced
--- /dev/null
+++ b/themes/Base_Canvas/dialogue/dialogue.gd
@@ -0,0 +1,5 @@
+extends Control
+
+
+func set_dialogue_label(msg:String):
+ %ChatMessageAI.text = msg
diff --git a/themes/Base_Canvas/grapic/grapic.gd b/themes/Base_Canvas/grapic/grapic.gd
new file mode 100644
index 00000000..1fede286
--- /dev/null
+++ b/themes/Base_Canvas/grapic/grapic.gd
@@ -0,0 +1,34 @@
+extends Node2D
+var dragging:bool #拖拽状态
+var v2_mouse:Vector2i #鼠标的偏差
+
+@onready var _ClickPolygon: CollisionPolygon2D = $"%ClickPolygon"
+
+func _physics_process(_delta: float) -> void:
+ _update_click_polygon()
+
+func _update_click_polygon() -> void:
+ var click_polygon: PackedVector2Array = _ClickPolygon.polygon
+ for vec_i in range(click_polygon.size()):
+ click_polygon[vec_i] = to_global(click_polygon[vec_i])
+ get_window().mouse_passthrough_polygon = click_polygon
+
+
+#拖拽窗口
+func _input(event):
+ if event is InputEventMouseButton:
+ if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
+ dragging = true
+ v2_mouse =get_global_mouse_position()
+ else:
+ dragging = false
+
+ if event is InputEventMouseMotion and dragging:
+ #窗口位置=鼠标位置 - 鼠标的偏差值
+ DisplayServer.window_set_position(DisplayServer.mouse_get_position()-v2_mouse)
+
+ if event is InputEventMouseButton:
+ if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
+ var SEND := load("res://send/send.tscn")
+ var send = SEND.instantiate()
+ get_node('/root/App').add_child(send)
diff --git a/themes/Base_Canvas/main.tres b/themes/Base_Canvas/main.tres
new file mode 100644
index 00000000..a22426b8
--- /dev/null
+++ b/themes/Base_Canvas/main.tres
@@ -0,0 +1,10 @@
+[gd_resource type="Theme" load_steps=3 format=3 uid="uid://7wp65kjcrv2s"]
+
+[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_lj7mo"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ddhj5"]
+
+[resource]
+RichTextLabel/colors/default_color = Color(1, 0.996094, 0.996094, 1)
+RichTextLabel/styles/normal = SubResource("StyleBoxTexture_lj7mo")
+VScrollBar/styles/scroll = SubResource("StyleBoxEmpty_ddhj5")
diff --git a/themes/Tiny_Swords/assets/characters/Warrior_Blue.png b/themes/Tiny_Swords/assets/characters/Warrior_Blue.png
new file mode 100644
index 00000000..10481456
Binary files /dev/null and b/themes/Tiny_Swords/assets/characters/Warrior_Blue.png differ
diff --git a/themes/Tiny_Swords/assets/characters/Warrior_Blue.png.import b/themes/Tiny_Swords/assets/characters/Warrior_Blue.png.import
new file mode 100644
index 00000000..4347814b
--- /dev/null
+++ b/themes/Tiny_Swords/assets/characters/Warrior_Blue.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d2fc18wxmmf6e"
+path="res://.godot/imported/Warrior_Blue.png-f2aaf8f74a8b3cbda3ffe2fc9ad0323f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://themes/Tiny_Swords/assets/characters/Warrior_Blue.png"
+dest_files=["res://.godot/imported/Warrior_Blue.png-f2aaf8f74a8b3cbda3ffe2fc9ad0323f.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/themes/Tiny_Swords/assets/dialogue/Banner_Vertical.png b/themes/Tiny_Swords/assets/dialogue/Banner_Vertical.png
new file mode 100644
index 00000000..e9076f82
Binary files /dev/null and b/themes/Tiny_Swords/assets/dialogue/Banner_Vertical.png differ
diff --git a/themes/Tiny_Swords/assets/dialogue/Banner_Vertical.png.import b/themes/Tiny_Swords/assets/dialogue/Banner_Vertical.png.import
new file mode 100644
index 00000000..0a1a0c9f
--- /dev/null
+++ b/themes/Tiny_Swords/assets/dialogue/Banner_Vertical.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ckkx6v5ckrfpd"
+path="res://.godot/imported/Banner_Vertical.png-698e4220a9a76f2ba26cce4ea2b2b212.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://themes/Tiny_Swords/assets/dialogue/Banner_Vertical.png"
+dest_files=["res://.godot/imported/Banner_Vertical.png-698e4220a9a76f2ba26cce4ea2b2b212.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/themes/Tiny_Swords/assets/fonts/LXGWNeoXiHei.ttf b/themes/Tiny_Swords/assets/fonts/LXGWNeoXiHei.ttf
new file mode 100644
index 00000000..db9ed7e1
Binary files /dev/null and b/themes/Tiny_Swords/assets/fonts/LXGWNeoXiHei.ttf differ
diff --git a/themes/Tiny_Swords/assets/fonts/LXGWNeoXiHei.ttf.import b/themes/Tiny_Swords/assets/fonts/LXGWNeoXiHei.ttf.import
new file mode 100644
index 00000000..41516316
--- /dev/null
+++ b/themes/Tiny_Swords/assets/fonts/LXGWNeoXiHei.ttf.import
@@ -0,0 +1,33 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://4ox4ebvuv7qk"
+path="res://.godot/imported/LXGWNeoXiHei.ttf-b4e9cda6f06c822cd3af1dfb9f213e79.fontdata"
+
+[deps]
+
+source_file="res://themes/Tiny_Swords/assets/fonts/LXGWNeoXiHei.ttf"
+dest_files=["res://.godot/imported/LXGWNeoXiHei.ttf-b4e9cda6f06c822cd3af1dfb9f213e79.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=1
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/themes/Tiny_Swords/assets/fonts/SmileySans-Oblique.ttf b/themes/Tiny_Swords/assets/fonts/SmileySans-Oblique.ttf
new file mode 100644
index 00000000..c297dc69
Binary files /dev/null and b/themes/Tiny_Swords/assets/fonts/SmileySans-Oblique.ttf differ
diff --git a/themes/Tiny_Swords/assets/fonts/SmileySans-Oblique.ttf.import b/themes/Tiny_Swords/assets/fonts/SmileySans-Oblique.ttf.import
new file mode 100644
index 00000000..af7df832
--- /dev/null
+++ b/themes/Tiny_Swords/assets/fonts/SmileySans-Oblique.ttf.import
@@ -0,0 +1,33 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://kmj5xlo1evt2"
+path="res://.godot/imported/SmileySans-Oblique.ttf-b7baff69bf94db9c1d21f32f77fb1cca.fontdata"
+
+[deps]
+
+source_file="res://themes/Tiny_Swords/assets/fonts/SmileySans-Oblique.ttf"
+dest_files=["res://.godot/imported/SmileySans-Oblique.ttf-b7baff69bf94db9c1d21f32f77fb1cca.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=1
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/themes/Tiny_Swords/assets/fonts/SourceHanSansSC-Normal.otf b/themes/Tiny_Swords/assets/fonts/SourceHanSansSC-Normal.otf
new file mode 100644
index 00000000..3cb93369
Binary files /dev/null and b/themes/Tiny_Swords/assets/fonts/SourceHanSansSC-Normal.otf differ
diff --git a/themes/Tiny_Swords/assets/fonts/SourceHanSansSC-Normal.otf.import b/themes/Tiny_Swords/assets/fonts/SourceHanSansSC-Normal.otf.import
new file mode 100644
index 00000000..86790306
--- /dev/null
+++ b/themes/Tiny_Swords/assets/fonts/SourceHanSansSC-Normal.otf.import
@@ -0,0 +1,33 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://c7rot5p106nta"
+path="res://.godot/imported/SourceHanSansSC-Normal.otf-b18de7fefbe9ec0a4c694354c59a4f1c.fontdata"
+
+[deps]
+
+source_file="res://themes/Tiny_Swords/assets/fonts/SourceHanSansSC-Normal.otf"
+dest_files=["res://.godot/imported/SourceHanSansSC-Normal.otf-b18de7fefbe9ec0a4c694354c59a4f1c.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=1
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/themes/Tiny_Swords/dialogue/dialogue.gd b/themes/Tiny_Swords/dialogue/dialogue.gd
new file mode 100644
index 00000000..05693ced
--- /dev/null
+++ b/themes/Tiny_Swords/dialogue/dialogue.gd
@@ -0,0 +1,5 @@
+extends Control
+
+
+func set_dialogue_label(msg:String):
+ %ChatMessageAI.text = msg
diff --git a/themes/Tiny_Swords/grapic/grapic.gd b/themes/Tiny_Swords/grapic/grapic.gd
new file mode 100644
index 00000000..43b09a0d
--- /dev/null
+++ b/themes/Tiny_Swords/grapic/grapic.gd
@@ -0,0 +1,36 @@
+extends Node2D
+var dragging:bool #拖拽状态
+var v2_mouse:Vector2i #鼠标的偏差
+
+@onready var _ClickPolygon: CollisionPolygon2D = $"%ClickPolygon"
+const SEND = preload("res://send/send.tscn")
+
+func _physics_process(_delta: float) -> void:
+ _update_click_polygon()
+
+func _update_click_polygon() -> void:
+ var click_polygon: PackedVector2Array = _ClickPolygon.polygon
+ for vec_i in range(click_polygon.size()):
+ click_polygon[vec_i] = to_global(click_polygon[vec_i])
+ get_window().mouse_passthrough_polygon = click_polygon
+
+
+#拖拽窗口
+func _input(event):
+ if event is InputEventMouseButton:
+ if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
+ dragging = true
+ v2_mouse =get_global_mouse_position()
+ else:
+ dragging = false
+
+ if event is InputEventMouseMotion and dragging:
+ #窗口位置=鼠标位置 - 鼠标的偏差值
+ DisplayServer.window_set_position(DisplayServer.mouse_get_position()-v2_mouse)
+
+ if event is InputEventMouseButton:
+ if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
+ var send = SEND.instantiate()
+ get_node('/root/App').add_child(send)
+
+
diff --git a/themes/Tiny_Swords/tiny_swords.md b/themes/Tiny_Swords/tiny_swords.md
new file mode 100644
index 00000000..e69de29b
diff --git a/themes/Tiny_Swords/tiny_swords_canvas.gd b/themes/Tiny_Swords/tiny_swords_canvas.gd
new file mode 100644
index 00000000..554c534c
--- /dev/null
+++ b/themes/Tiny_Swords/tiny_swords_canvas.gd
@@ -0,0 +1,46 @@
+extends Node2D
+
+@onready var state_change_timer : Timer = $StateChange
+@onready var animation_player: AnimationPlayer = $Grapic/Pet/AnimationPlayer
+@onready var dialogue_timer: Timer = $Dialogue_Timer
+@onready var dialogue_node: Control = %Dialogue
+
+
+
+var animation_list := []
+
+func _ready() -> void:
+ _initialize_window()
+ animation_list = animation_player.get_animation_list()
+ animation_list.remove_at(animation_list.find('idle'))
+ animation_list.remove_at(animation_list.find('RESET'))
+ state_change_timer.start()
+ state_change_timer.timeout.connect(_on_state_change)
+
+ dialogue_timer.timeout.connect(_on_dialogue_timer)
+
+func _initialize_window() -> void:
+ var window: Window = get_window()
+ window.size = Vector2i(DisplayServer.screen_get_size() + Vector2i(1, 1))
+ window.position = DisplayServer.screen_get_position()
+
+
+func set_dialogue_label(msg:String):
+ %Dialogue.set_dialogue_label(msg)
+
+func _on_state_change():
+ var idx = randi_range(0,animation_list.size()-1)
+ animation_player.play(animation_list[idx])
+
+ await animation_player.animation_finished
+ animation_player.play('idle')
+ state_change_timer.start(randf_range(30,120))
+
+# 20s内没有任何操作,自动隐藏对话节点 ; 有操作就重新记时
+func start_hide_dialogue():
+ dialogue_node.show()
+
+ dialogue_timer.start(20)
+
+func _on_dialogue_timer():
+ dialogue_node.hide()
diff --git a/themes/Tiny_Swords/tiny_swords_canvas.tres b/themes/Tiny_Swords/tiny_swords_canvas.tres
new file mode 100644
index 00000000..ab9fa507
--- /dev/null
+++ b/themes/Tiny_Swords/tiny_swords_canvas.tres
@@ -0,0 +1,13 @@
+[gd_resource type="Theme" load_steps=4 format=3 uid="uid://bn50jp1pavmmx"]
+
+[ext_resource type="FontFile" uid="uid://4ox4ebvuv7qk" path="res://themes/Tiny_Swords/assets/fonts/LXGWNeoXiHei.ttf" id="1_h3w4b"]
+
+[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_lj7mo"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ddhj5"]
+
+[resource]
+default_font = ExtResource("1_h3w4b")
+RichTextLabel/colors/default_color = Color(1, 0.996094, 0.996094, 1)
+RichTextLabel/styles/normal = SubResource("StyleBoxTexture_lj7mo")
+VScrollBar/styles/scroll = SubResource("StyleBoxEmpty_ddhj5")
diff --git a/themes/Tiny_Swords/tiny_swords_canvas.tscn b/themes/Tiny_Swords/tiny_swords_canvas.tscn
new file mode 100644
index 00000000..2f6a5f18
--- /dev/null
+++ b/themes/Tiny_Swords/tiny_swords_canvas.tscn
@@ -0,0 +1,224 @@
+[gd_scene load_steps=17 format=3 uid="uid://u4f1it53avo1"]
+
+[ext_resource type="Script" path="res://themes/Tiny_Swords/tiny_swords_canvas.gd" id="1_k04u4"]
+[ext_resource type="Script" path="res://themes/Tiny_Swords/grapic/grapic.gd" id="2_ff0a0"]
+[ext_resource type="Texture2D" uid="uid://d2fc18wxmmf6e" path="res://themes/Tiny_Swords/assets/characters/Warrior_Blue.png" id="3_mrsu2"]
+[ext_resource type="Theme" uid="uid://bn50jp1pavmmx" path="res://themes/Tiny_Swords/tiny_swords_canvas.tres" id="4_0s2uf"]
+[ext_resource type="Script" path="res://themes/Tiny_Swords/dialogue/dialogue.gd" id="4_8v2p0"]
+[ext_resource type="Texture2D" uid="uid://ckkx6v5ckrfpd" path="res://themes/Tiny_Swords/assets/dialogue/Banner_Vertical.png" id="5_wpqw3"]
+[ext_resource type="Script" path="res://themes/Base_Canvas/dialogue/chat_message_ai.gd" id="7_0038y"]
+
+[sub_resource type="Animation" id="Animation_75tyo"]
+length = 0.001
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath(".:frame")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 1,
+"values": [0]
+}
+
+[sub_resource type="Animation" id="Animation_4elpe"]
+resource_name = "attack_down"
+length = 0.9
+step = 0.15
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath(".:frame")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 0.15, 0.3, 0.45, 0.6, 0.75),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1),
+"update": 1,
+"values": [12, 13, 14, 15, 16, 17]
+}
+
+[sub_resource type="Animation" id="Animation_aknv2"]
+resource_name = "attack_left"
+length = 0.9
+step = 0.15
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath(".:frame")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 0.15, 0.3, 0.45, 0.6, 0.75),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1),
+"update": 1,
+"values": [24, 25, 26, 27, 28, 29]
+}
+
+[sub_resource type="Animation" id="Animation_rhkkn"]
+resource_name = "attack_right"
+length = 0.9
+step = 0.15
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath(".:frame")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 0.15, 0.3, 0.45, 0.6, 0.75),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1),
+"update": 1,
+"values": [30, 31, 32, 33, 34, 35]
+}
+
+[sub_resource type="Animation" id="Animation_3unuo"]
+resource_name = "attack_up"
+length = 0.9
+step = 0.15
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath(".:frame")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 0.15, 0.3, 0.45, 0.6, 0.75),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1),
+"update": 1,
+"values": [18, 19, 20, 21, 22, 23]
+}
+
+[sub_resource type="Animation" id="Animation_kb6fg"]
+resource_name = "idle"
+length = 0.9
+loop_mode = 1
+step = 0.15
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath(".:frame")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 0.15, 0.3, 0.45, 0.6, 0.75),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1),
+"update": 1,
+"values": [0, 1, 2, 3, 4, 5]
+}
+
+[sub_resource type="Animation" id="Animation_8umtp"]
+resource_name = "run"
+length = 0.9
+step = 0.15
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath(".:frame")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 0.15, 0.3, 0.45, 0.6, 0.75),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1),
+"update": 1,
+"values": [6, 7, 8, 9, 10, 11]
+}
+
+[sub_resource type="AnimationLibrary" id="AnimationLibrary_5hx07"]
+_data = {
+"RESET": SubResource("Animation_75tyo"),
+"attack_down": SubResource("Animation_4elpe"),
+"attack_left": SubResource("Animation_aknv2"),
+"attack_right": SubResource("Animation_rhkkn"),
+"attack_up": SubResource("Animation_3unuo"),
+"idle": SubResource("Animation_kb6fg"),
+"run": SubResource("Animation_8umtp")
+}
+
+[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_o4c6w"]
+texture = ExtResource("5_wpqw3")
+region_rect = Rect2(34.6328, 30.5003, 120.784, 132.116)
+
+[node name="TinySwordsCanvas" type="Node2D"]
+position = Vector2(2373, 152)
+script = ExtResource("1_k04u4")
+metadata/_edit_lock_ = true
+metadata/_edit_group_ = true
+
+[node name="Grapic" type="Node2D" parent="."]
+scale = Vector2(-2, 2)
+script = ExtResource("2_ff0a0")
+
+[node name="Pet" type="Sprite2D" parent="Grapic"]
+position = Vector2(-1, 33.5)
+texture = ExtResource("3_mrsu2")
+hframes = 6
+vframes = 8
+
+[node name="AnimationPlayer" type="AnimationPlayer" parent="Grapic/Pet"]
+libraries = {
+"": SubResource("AnimationLibrary_5hx07")
+}
+autoplay = "idle"
+next/attack_up = &"RESET"
+
+[node name="Area2D" type="Area2D" parent="Grapic"]
+position = Vector2(2, 9)
+
+[node name="ClickPolygon" type="CollisionPolygon2D" parent="Grapic/Area2D"]
+unique_name_in_owner = true
+self_modulate = Color(1, 1, 1, 0.105882)
+polygon = PackedVector2Array(276, 91, -65, 91, -64, -82.5, 274.5, -83.5)
+disabled = true
+
+[node name="StateChange" type="Timer" parent="."]
+wait_time = 30.0
+
+[node name="Dialogue_Timer" type="Timer" parent="."]
+wait_time = 20.0
+one_shot = true
+autostart = true
+
+[node name="Dialogue" type="Control" parent="."]
+unique_name_in_owner = true
+layout_mode = 3
+anchors_preset = 0
+offset_left = -514.0
+offset_top = -214.0
+offset_right = -145.0
+offset_bottom = 227.0
+scale = Vector2(0.8, 0.8)
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme = ExtResource("4_0s2uf")
+script = ExtResource("4_8v2p0")
+
+[node name="PanelContainer" type="PanelContainer" parent="Dialogue"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 56.0
+offset_top = 84.0
+offset_right = 100.0
+offset_bottom = 70.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_styles/panel = SubResource("StyleBoxTexture_o4c6w")
+
+[node name="MarginContainer" type="MarginContainer" parent="Dialogue/PanelContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 64
+theme_override_constants/margin_top = 47
+theme_override_constants/margin_right = 47
+theme_override_constants/margin_bottom = 80
+
+[node name="ChatMessageAI" type="RichTextLabel" parent="Dialogue/PanelContainer/MarginContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_colors/default_color = Color(0, 0, 0, 1)
+theme_override_font_sizes/normal_font_size = 25
+bbcode_enabled = true
+script = ExtResource("7_0038y")