nova-orbit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+
2
+ #### 中文版 `README_zh.md`
3
+
4
+ ```markdown
5
+ # Nova-Orbit 🪐
6
+
7
+ > **专为 AI Agent 打造的桌面端终极聊天框架。**
8
+ > *GPU 极速渲染,超乎想象的丝滑与美貌。基于 PySide6 与 QML 构建。*
9
+
10
+ [English](README.md) | **简体中文**
11
+
12
+ ## 🤔 为什么选择 Nova-Orbit?
13
+
14
+ 基于 Web 的 AI 客户端(如 Gradio)往往卡顿且体验割裂。**Nova-Orbit** 将原生桌面体验与 AI 完美融合:
15
+
16
+ - 🌱 **“生长式”流式渲染**:文字逐字出现时,气泡高度平滑自适应,滚动条智能跟随,绝不掉帧。
17
+ - 🟩 **原生系统日志**:荧光绿极客日志与聊天气泡共存,完美契合 Agent 的思考与执行过程。
18
+ - 🖱️ **原生桌面交互**:支持原生快捷键、拖拽附件,纯粹的暗黑美学。
19
+
20
+ ## 🚀 安装与使用(仅需 10 行代码)
21
+
22
+ ```bash
23
+ pip install nova-orbit
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: nova-orbit
3
+ Version: 0.1.0
4
+ Summary: A stunning, GPU-accelerated AI chat UI framework for PySide6/QML.
5
+ Author-email: eagleye-china <zdt168@sina.cn>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/eagleye-china/nova-orbit
8
+ Project-URL: Repository, https://github.com/eagleye-china/nova-orbit
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: PySide6>=6.6
13
+ Requires-Dist: openai>=1.0
14
+ Requires-Dist: markdown>=3.5
15
+ Dynamic: license-file
16
+
17
+ # Nova-Orbit 🪐
18
+
19
+ > **The Ultimate Desktop Chat UI Framework for AI Agents.**
20
+ > *GPU-accelerated, stunningly beautiful, and unbelievably fast. Built on PySide6 & QML.*
21
+
22
+ [![PyPI version](https://img.shields.io/badge/pypi-coming_soon-orange)]()
23
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)]()
24
+ [![License](https://img.shields.io/badge/license-MIT-green)]()
25
+
26
+ **English** | [简体中文](README_zh.md)
27
+
28
+ ## 🤔 Why Nova-Orbit?
29
+
30
+ Web-based AI apps (like Gradio or Streamlit) are slow and clunky. **Nova-Orbit** brings AI directly to your desktop with zero compromise:
31
+
32
+ - 🌱 **"Growing" Streaming**: Bubbles dynamically resize as text generates. Smooth, jank-free scrolling powered by QML.
33
+ - 🟩 **Integrated System Logs**: Neon green terminal-style logs live natively alongside chat bubbles. Perfect for Agentic workflows.
34
+ - 🖱️ **Native Desktop Feel**: Drag & drop files, native shortcuts, and pure dark mode aesthetics.
35
+
36
+ ## 🚀 Install & Use (10 Lines of Code)
37
+
38
+ ```bash
39
+ pip install nova-orbit
@@ -0,0 +1,23 @@
1
+ # Nova-Orbit 🪐
2
+
3
+ > **The Ultimate Desktop Chat UI Framework for AI Agents.**
4
+ > *GPU-accelerated, stunningly beautiful, and unbelievably fast. Built on PySide6 & QML.*
5
+
6
+ [![PyPI version](https://img.shields.io/badge/pypi-coming_soon-orange)]()
7
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)]()
8
+ [![License](https://img.shields.io/badge/license-MIT-green)]()
9
+
10
+ **English** | [简体中文](README_zh.md)
11
+
12
+ ## 🤔 Why Nova-Orbit?
13
+
14
+ Web-based AI apps (like Gradio or Streamlit) are slow and clunky. **Nova-Orbit** brings AI directly to your desktop with zero compromise:
15
+
16
+ - 🌱 **"Growing" Streaming**: Bubbles dynamically resize as text generates. Smooth, jank-free scrolling powered by QML.
17
+ - 🟩 **Integrated System Logs**: Neon green terminal-style logs live natively alongside chat bubbles. Perfect for Agentic workflows.
18
+ - 🖱️ **Native Desktop Feel**: Drag & drop files, native shortcuts, and pure dark mode aesthetics.
19
+
20
+ ## 🚀 Install & Use (10 Lines of Code)
21
+
22
+ ```bash
23
+ pip install nova-orbit
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.2", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nova-orbit"
7
+ version = "0.1.0"
8
+ description = "A stunning, GPU-accelerated AI chat UI framework for PySide6/QML."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "eagleye-china", email = "zdt168@sina.cn" }]
13
+ dependencies = [
14
+ "PySide6>=6.6",
15
+ "openai>=1.0",
16
+ "markdown>=3.5"
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/eagleye-china/nova-orbit"
21
+ Repository = "https://github.com/eagleye-china/nova-orbit"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+
26
+ # ✅ 关键:将 QML 文件打包进库中
27
+ [tool.setuptools.package-data]
28
+ nova_orbit = ["qml/*.qml"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ from .widget import NovaChatWidget
2
+ from .bridge import NovaChatBridge
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = ["NovaChatWidget", "NovaChatBridge"]
@@ -0,0 +1,109 @@
1
+ import threading
2
+ from PySide6.QtCore import QObject, Slot, Property, QAbstractListModel, QModelIndex, Qt, QTimer
3
+ from openai import OpenAI
4
+
5
+
6
+ class MessageModel(QAbstractListModel):
7
+ TextRole = Qt.ItemDataRole.UserRole + 1
8
+ IsUserRole = Qt.ItemDataRole.UserRole + 2
9
+ IsSystemRole = Qt.ItemDataRole.UserRole + 3
10
+
11
+ def __init__(self):
12
+ super().__init__()
13
+ self._messages = []
14
+
15
+ def rowCount(self, parent=QModelIndex()):
16
+ return len(self._messages)
17
+
18
+ def data(self, index, role):
19
+ if not index.isValid(): return None
20
+ msg = self._messages[index.row()]
21
+ if role == self.TextRole: return msg.get("text", "")
22
+ if role == self.IsUserRole: return msg.get("isUser", False)
23
+ if role == self.IsSystemRole: return msg.get("isSystem", False)
24
+ return None
25
+
26
+ def roleNames(self):
27
+ return {
28
+ self.TextRole: b"text",
29
+ self.IsUserRole: b"isUser",
30
+ self.IsSystemRole: b"isSystem"
31
+ }
32
+
33
+ def add(self, text, is_user=False, is_sys=False):
34
+ self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
35
+ self._messages.append({"text": text, "isUser": is_user, "isSystem": is_sys})
36
+ self.endInsertRows()
37
+
38
+ def update_last(self, text):
39
+ if self._messages:
40
+ self._messages[-1]["text"] = text
41
+ idx = self.index(len(self._messages) - 1, 0)
42
+ self.dataChanged.emit(idx, idx, [self.TextRole])
43
+
44
+
45
+ class NovaChatBridge(QObject):
46
+ """核心桥接器:处理 API 请求、数据模型与 QML 的交互"""
47
+
48
+ def __init__(self, api_key: str, base_url="https://api.deepseek.com", model="deepseek-chat"):
49
+ super().__init__()
50
+ self.api_key = api_key
51
+ self.base_url = base_url
52
+ self.model_name = model
53
+ self.messages = [{"role": "system", "content": "You are a helpful assistant."}]
54
+
55
+ self._model = MessageModel()
56
+ self._model.add("Hello! I am Nova-Orbit. How can I assist you today?", False)
57
+
58
+ self._client = OpenAI(api_key=self.api_key, base_url=self.base_url)
59
+ self._current_ai_text = ""
60
+ self._is_streaming = False
61
+
62
+ self._timer = QTimer()
63
+ self._timer.setInterval(50)
64
+ self._timer.timeout.connect(self._update_ui_stream)
65
+
66
+ @Property(QObject, constant=True)
67
+ def messagesModel(self):
68
+ return self._model
69
+
70
+ @Slot(str)
71
+ def send_user_message(self, text):
72
+ if not text.strip() or self._is_streaming: return
73
+
74
+ self._model.add(text, True)
75
+ self.messages.append({"role": "user", "content": text})
76
+
77
+ self._current_ai_text = ""
78
+ self._model.add("Thinking...", False)
79
+ self._is_streaming = True
80
+ self._timer.start()
81
+
82
+ threading.Thread(target=self._fetch_ai_response, daemon=True).start()
83
+
84
+ @Slot(str)
85
+ def add_system_log(self, text):
86
+ """对外暴露的接口,可用于记录 Agent 的思考过程或工具调用"""
87
+ self._model.add(f"⚙️ {text}", False, is_sys=True)
88
+
89
+ def _fetch_ai_response(self):
90
+ try:
91
+ response = self._client.chat.completions.create(
92
+ model=self.model_name,
93
+ messages=self.messages,
94
+ stream=True
95
+ )
96
+ for chunk in response:
97
+ if chunk.choices and chunk.choices[0].delta.content:
98
+ self._current_ai_text += chunk.choices[0].delta.content
99
+ self.messages.append({"role": "assistant", "content": self._current_ai_text})
100
+ except Exception as e:
101
+ self._current_ai_text = f"⚠️ API Error: {str(e)}"
102
+ finally:
103
+ self._is_streaming = False
104
+
105
+ def _update_ui_stream(self):
106
+ if self._current_ai_text:
107
+ self._model.update_last(self._current_ai_text)
108
+ if not self._is_streaming and self._current_ai_text:
109
+ self._timer.stop()
@@ -0,0 +1,6 @@
1
+ import QtQuick
2
+ import QtQuick.Controls
3
+
4
+ Item {
5
+ // 附件预览区域逻辑,待完善模型绑定
6
+ }
@@ -0,0 +1,44 @@
1
+ import QtQuick
2
+ import QtQuick.Controls
3
+ import QtQuick.Layouts
4
+
5
+ Item {
6
+ width: 800; height: 600
7
+ property color bgColor: "#121212"
8
+
9
+ Rectangle {
10
+ anchors.fill: parent
11
+ color: bgColor
12
+
13
+ ColumnLayout {
14
+ anchors.fill: parent
15
+ anchors.margins: 15
16
+ spacing: 10
17
+
18
+ ListView {
19
+ id: listView
20
+ Layout.fillWidth: true
21
+ Layout.fillHeight: true
22
+ model: bridge.messagesModel
23
+ spacing: 15
24
+ clip: true
25
+
26
+ delegate: NovaMessageBubble {
27
+ width: listView.width
28
+ text: model.text
29
+ isUser: model.isUser
30
+ isSystem: model.isSystem
31
+ }
32
+ onCountChanged: listView.positionViewAtEnd()
33
+ }
34
+
35
+ NovaInputBar {
36
+ id: inputBar
37
+ Layout.fillWidth: true
38
+ onSendMessage: (text) => {
39
+ bridge.send_user_message(text)
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,79 @@
1
+ import QtQuick
2
+ import QtQuick.Controls
3
+ import QtQuick.Layouts
4
+
5
+ Item {
6
+ id: root
7
+ signal sendMessage(string text)
8
+ height: Math.min(150, Math.max(60, inputScroll.contentHeight + 20))
9
+
10
+ Rectangle {
11
+ anchors.fill: parent
12
+ radius: 8
13
+ color: "#263238"
14
+ border.color: inputArea.activeFocus ? "#0277BD" : "#37474F"
15
+ border.width: 1
16
+ Behavior on border.color { ColorAnimation { duration: 200 } }
17
+
18
+ ScrollView {
19
+ id: inputScroll
20
+ anchors.left: parent.left
21
+ anchors.right: sendBtn.left
22
+ anchors.top: parent.top
23
+ anchors.bottom: parent.bottom
24
+ anchors.leftMargin: 12
25
+ anchors.rightMargin: 10
26
+ clip: true
27
+
28
+ TextArea {
29
+ id: inputArea
30
+ placeholderText: "Type a message... (Enter to send, Shift+Enter for new line)"
31
+ color: "#ECEFF1"
32
+ font.pixelSize: 14
33
+ wrapMode: TextArea.Wrap
34
+ background: Item {}
35
+ selectByMouse: true
36
+
37
+ Keys.onPressed: (event) => {
38
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
39
+ if (event.modifiers & Qt.ShiftModifier) {
40
+ event.accepted = false;
41
+ } else {
42
+ if (inputArea.text.trim() !== "") {
43
+ root.sendMessage(inputArea.text.trim());
44
+ inputArea.text = "";
45
+ }
46
+ event.accepted = true;
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ Button {
54
+ id: sendBtn
55
+ text: "🚀"
56
+ anchors.right: parent.right
57
+ anchors.rightMargin: 10
58
+ anchors.bottom: parent.bottom
59
+ anchors.bottomMargin: 10
60
+ width: 40
61
+ height: 40
62
+ background: Rectangle { color: "#0277BD"; radius: 6 }
63
+ contentItem: Text { text: "🚀"; anchors.centerIn: parent; font.pixelSize: 18 }
64
+
65
+ visible: opacity > 0
66
+ opacity: inputArea.text.trim() !== "" ? 1.0 : 0.0
67
+ scale: inputArea.text.trim() !== "" ? 1.0 : 0.5
68
+ Behavior on opacity { NumberAnimation { duration: 150 } }
69
+ Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutBack } }
70
+
71
+ onClicked: {
72
+ if (inputArea.text.trim() !== "") {
73
+ root.sendMessage(inputArea.text.trim());
74
+ inputArea.text = "";
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,34 @@
1
+ import QtQuick
2
+ import QtQuick.Controls
3
+
4
+ Item {
5
+ id: root
6
+ property string text: ""
7
+ property bool isUser: false
8
+ property bool isSystem: false
9
+ height: bubbleRect.height + 10
10
+
11
+ Rectangle {
12
+ id: bubbleRect
13
+ anchors.right: isUser ? parent.right : undefined
14
+ anchors.left: isUser ? undefined : parent.left
15
+ width: Math.min(bubbleText.implicitWidth + 30, root.width * 0.75)
16
+ height: bubbleText.implicitHeight + 24
17
+ radius: isSystem ? 4 : 10
18
+ color: isUser ? "#0277BD" : (isSystem ? "#1E272E" : "#263238")
19
+ border.color: isSystem ? "#2E7D32" : "transparent"
20
+ border.width: isSystem ? 1 : 0
21
+
22
+ Text {
23
+ id: bubbleText
24
+ text: root.text || ""
25
+ anchors.centerIn: parent
26
+ width: parent.width - 30
27
+ wrapMode: Text.Wrap
28
+ color: isSystem ? "#4CAF50" : "#ECEFF1"
29
+ font.pixelSize: isSystem ? 12 : 14
30
+ font.family: isSystem ? "Consolas" : "Microsoft YaHei"
31
+ textFormat: Text.PlainText
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ import os
2
+ from PySide6.QtWidgets import QWidget, QVBoxLayout
3
+ from PySide6.QtQuickWidgets import QQuickWidget
4
+ from PySide6.QtCore import QUrl
5
+ from .bridge import NovaChatBridge
6
+
7
+
8
+ class NovaChatWidget(QWidget):
9
+ """开箱即用的 QWidget 封装,支持直接嵌入任何 PySide 应用"""
10
+
11
+ def __init__(self, api_key: str, parent=None):
12
+ super().__init__(parent)
13
+ self.setStyleSheet("background-color: #121212;")
14
+
15
+ layout = QVBoxLayout(self)
16
+ layout.setContentsMargins(0, 0, 0, 0)
17
+
18
+ self.quick_widget = QQuickWidget()
19
+ self.quick_widget.setResizeMode(QQuickWidget.ResizeMode.SizeRootObjectToView)
20
+
21
+ self.bridge = NovaChatBridge(api_key=api_key)
22
+
23
+ # 将 Python 桥接对象注入到 QML 环境
24
+ engine = self.quick_widget.engine()
25
+ engine.rootContext().setContextProperty("bridge", self.bridge)
26
+
27
+ # 加载打包在库里的 QML 文件
28
+ qml_path = os.path.join(os.path.dirname(__file__), "qml", "NovaChatWindow.qml")
29
+ self.quick_widget.setSource(QUrl.fromLocalFile(qml_path))
30
+
31
+ layout.addWidget(self.quick_widget)
32
+
33
+ # 暴露日志接口给外部
34
+ self.add_log = self.bridge.add_system_log
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: nova-orbit
3
+ Version: 0.1.0
4
+ Summary: A stunning, GPU-accelerated AI chat UI framework for PySide6/QML.
5
+ Author-email: eagleye-china <zdt168@sina.cn>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/eagleye-china/nova-orbit
8
+ Project-URL: Repository, https://github.com/eagleye-china/nova-orbit
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: PySide6>=6.6
13
+ Requires-Dist: openai>=1.0
14
+ Requires-Dist: markdown>=3.5
15
+ Dynamic: license-file
16
+
17
+ # Nova-Orbit 🪐
18
+
19
+ > **The Ultimate Desktop Chat UI Framework for AI Agents.**
20
+ > *GPU-accelerated, stunningly beautiful, and unbelievably fast. Built on PySide6 & QML.*
21
+
22
+ [![PyPI version](https://img.shields.io/badge/pypi-coming_soon-orange)]()
23
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)]()
24
+ [![License](https://img.shields.io/badge/license-MIT-green)]()
25
+
26
+ **English** | [简体中文](README_zh.md)
27
+
28
+ ## 🤔 Why Nova-Orbit?
29
+
30
+ Web-based AI apps (like Gradio or Streamlit) are slow and clunky. **Nova-Orbit** brings AI directly to your desktop with zero compromise:
31
+
32
+ - 🌱 **"Growing" Streaming**: Bubbles dynamically resize as text generates. Smooth, jank-free scrolling powered by QML.
33
+ - 🟩 **Integrated System Logs**: Neon green terminal-style logs live natively alongside chat bubbles. Perfect for Agentic workflows.
34
+ - 🖱️ **Native Desktop Feel**: Drag & drop files, native shortcuts, and pure dark mode aesthetics.
35
+
36
+ ## 🚀 Install & Use (10 Lines of Code)
37
+
38
+ ```bash
39
+ pip install nova-orbit
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/nova_orbit/__init__.py
5
+ src/nova_orbit/bridge.py
6
+ src/nova_orbit/widget.py
7
+ src/nova_orbit.egg-info/PKG-INFO
8
+ src/nova_orbit.egg-info/SOURCES.txt
9
+ src/nova_orbit.egg-info/dependency_links.txt
10
+ src/nova_orbit.egg-info/requires.txt
11
+ src/nova_orbit.egg-info/top_level.txt
12
+ src/nova_orbit/qml/NovaAttachmentStrip.qml
13
+ src/nova_orbit/qml/NovaChatWindow.qml
14
+ src/nova_orbit/qml/NovaInputBar.qml
15
+ src/nova_orbit/qml/NovaMessageBubble.qml
@@ -0,0 +1,3 @@
1
+ PySide6>=6.6
2
+ openai>=1.0
3
+ markdown>=3.5
@@ -0,0 +1 @@
1
+ nova_orbit