diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json
index 87f05a6..98d8d1d 100644
--- a/app/src-tauri/tauri.conf.json
+++ b/app/src-tauri/tauri.conf.json
@@ -8,7 +8,7 @@
},
"package": {
"productName": "chatnio",
- "version": "3.7.5"
+ "version": "3.7.6"
},
"tauri": {
"allowlist": {
diff --git a/app/src/assets/admin/all.less b/app/src/assets/admin/all.less
index 3dbc8c4..6ad3bf5 100644
--- a/app/src/assets/admin/all.less
+++ b/app/src/assets/admin/all.less
@@ -4,6 +4,7 @@
@import "broadcast";
@import "channel";
@import "charge";
+@import "system";
.admin-page {
position: relative;
diff --git a/app/src/assets/admin/system.less b/app/src/assets/admin/system.less
new file mode 100644
index 0000000..6959dd7
--- /dev/null
+++ b/app/src/assets/admin/system.less
@@ -0,0 +1,13 @@
+.system {
+ width: 100%;
+ height: max-content;
+ padding: 2rem;
+ display: flex;
+ flex-direction: column;
+
+ .system-card {
+ width: 100%;
+ height: 100%;
+ min-height: 20vh;
+ }
+}
diff --git a/app/src/assets/ui.less b/app/src/assets/ui.less
index 10bd83e..b215619 100644
--- a/app/src/assets/ui.less
+++ b/app/src/assets/ui.less
@@ -123,3 +123,144 @@ input[type="number"] {
background: hsl(var(--selection));
}
}
+
+.paragraph {
+ display: flex;
+ flex-direction: column;
+ margin: 0.5rem 0;
+ padding: 1.5rem;
+ border-radius: var(--radius);
+ background: hsl(var(--background));
+ color: hsl(var(--text));
+ border: 1px solid hsl(var(--border));
+ transition: .25s;
+ cursor: pointer;
+
+ &.collapsable {
+ .paragraph-content {
+ max-height: var(--max-height);
+ will-change: max-height;
+ overflow: hidden;
+ transition: .5s;
+ }
+
+ &.collapsed {
+ padding-bottom: 1rem;
+
+ .paragraph-content {
+ max-height: 0;
+ }
+ }
+ }
+
+ .paragraph-content {
+ & > * {
+ margin-bottom: 1rem;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ .paragraph-header {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ margin-bottom: 1.5rem;
+ align-items: center;
+ transform: translateY(-0.25rem);
+ }
+
+ .paragraph-title {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ font-size: 1.05rem;
+ user-select: none;
+ line-height: 1.1rem;
+ color: hsl(var(--text-secondary));
+ transition: .25s;
+
+ &:before {
+ content: '';
+ margin-right: 0.5rem;
+ height: 1.25rem;
+ width: 2px;
+ border-radius: 1px;
+ background: hsl(var(--text-secondary));
+ transition: .25s;
+ }
+ }
+
+ .paragraph-item {
+ display: flex;
+ flex-direction: row;
+ white-space: nowrap;
+ align-items: center;
+ font-size: 0.9rem;
+
+ label {
+ font-size: 0.9rem;
+ font-weight: normal;
+ }
+
+ & > * {
+ margin-right: 1rem;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+
+ .paragraph-description {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ color: hsl(var(--text-secondary));
+ width: 100%;
+ height: max-content;
+ font-size: 0.9rem;
+ margin: 0.75rem 0;
+
+ svg {
+ margin-right: 0.5rem;
+ flex-shrink: 0;
+ }
+ }
+
+ .paragraph-footer {
+ display: flex;
+ flex-direction: row;
+ margin-top: 1rem;
+
+ & > * {
+ margin-right: 1rem;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+
+ &:hover {
+ border-color: hsl(var(--border-hover));
+
+ .paragraph-title {
+ color: hsl(var(--text));
+
+ &:before {
+ background: hsl(var(--text));
+ }
+ }
+ }
+
+ &.config-paragraph {
+ .paragraph-content {
+ input {
+ margin-left: auto;
+ }
+ }
+ }
+}
diff --git a/app/src/components/Paragraph.tsx b/app/src/components/Paragraph.tsx
new file mode 100644
index 0000000..05d5ec3
--- /dev/null
+++ b/app/src/components/Paragraph.tsx
@@ -0,0 +1,86 @@
+import React from "react";
+import { ChevronDown, Info } from "lucide-react";
+import { cn } from "@/components/ui/lib/utils.ts";
+import { Button } from "@/components/ui/button.tsx";
+import Markdown from "@/components/Markdown.tsx";
+
+export type ParagraphProps = {
+ title?: string;
+ children: React.ReactNode;
+ configParagraph?: boolean;
+ isCollapsed?: boolean;
+ onCollapse?: () => void;
+ defaultCollapsed?: boolean;
+};
+
+function Paragraph({
+ title,
+ children,
+ configParagraph,
+ isCollapsed,
+ onCollapse,
+ defaultCollapsed,
+}: ParagraphProps) {
+ const [collapsed, setCollapsed] = React.useState(defaultCollapsed ?? false);
+
+ React.useEffect(() => onCollapse && onCollapse(), [collapsed]);
+
+ return (
+
+
setCollapsed(!collapsed)}
+ >
+ {title &&
{title}
}
+
+ {isCollapsed && (
+
+ )}
+
+
+ {children}
+
+
+ );
+}
+
+function ParagraphItem({ children }: { children: React.ReactNode }) {
+ return {children}
;
+}
+
+export function ParagraphDescription({ children }: { children: string }) {
+ return (
+
+
+
+
+ );
+}
+
+function ParagraphFooter({ children }: { children: React.ReactNode }) {
+ return {children}
;
+}
+
+export default Paragraph;
+export { ParagraphItem, ParagraphFooter };
diff --git a/app/src/components/admin/ChargeWidget.tsx b/app/src/components/admin/ChargeWidget.tsx
index 3927412..f27160e 100644
--- a/app/src/components/admin/ChargeWidget.tsx
+++ b/app/src/components/admin/ChargeWidget.tsx
@@ -399,7 +399,7 @@ function ChargeTable({ data, dispatch, onRefresh }: ChargeTableProps) {
{charge.type.split("-")[0]}
-
+
{charge.models.join("\n")}
diff --git a/app/src/components/admin/MenuBar.tsx b/app/src/components/admin/MenuBar.tsx
index a2223d8..f4f3083 100644
--- a/app/src/components/admin/MenuBar.tsx
+++ b/app/src/components/admin/MenuBar.tsx
@@ -3,6 +3,7 @@ import { closeMenu, selectMenu } from "@/store/menu.ts";
import React, { useMemo } from "react";
import {
CandlestickChart,
+ GitFork,
LayoutDashboard,
Radio,
Settings,
@@ -60,7 +61,7 @@ function MenuBar() {
/>
}
+ icon={}
path={"/channel"}
/>
}
path={"/charge"}
/>
+ }
+ path={"/system"}
+ />
);
}
diff --git a/app/src/components/admin/assemblies/BroadcastTable.tsx b/app/src/components/admin/assemblies/BroadcastTable.tsx
index 10046fd..8040712 100644
--- a/app/src/components/admin/assemblies/BroadcastTable.tsx
+++ b/app/src/components/admin/assemblies/BroadcastTable.tsx
@@ -88,7 +88,7 @@ function CreateBroadcastDialog(props: CreateBroadcastDialogProps) {
-