11<script setup lang="ts">
2- import { ref , computed } from ' vue'
2+ import { ref } from ' vue'
33import { useI18n } from ' vue-i18n'
4- import { apiFetch } from ' @/shared/api '
5- import { useToast } from ' sbot-ui'
4+ import { useToast , SModal , SButton } from ' sbot-ui '
5+ import { FileExplorer , WebSocketTransport } from ' @ sbot/chat -ui'
66import { sourceBadgeStyle } from ' @/utils/badges'
7- import { SModal , SButton , STree , STreeNode } from ' sbot-ui'
87
98const { t } = useI18n ()
109const { show } = useToast ()
1110
12- // ── State ────────────────────────────────────────────────────────
1311const visible = ref (false )
1412const skillName = ref (' ' )
1513const skillBadge = ref (' ' )
16- const skillApiBase = ref (' /api/skills' )
17-
18- // ── Tree ─────────────────────────────────────────────────────────
19- type TreeNode = { name: string ; type: ' file' | ' dir' ; path: string ; children? : TreeNode [] }
20-
21- const tree = ref <TreeNode []>([])
22- const collapsed = ref (new Set <string >())
23-
24- function flattenTree(nodes : TreeNode [], depth = 0 ): { node: TreeNode ; depth: number }[] {
25- const result: { node: TreeNode ; depth: number }[] = []
26- for (const node of nodes ) {
27- result .push ({ node , depth })
28- if (node .type === ' dir' && ! collapsed .value .has (node .path )) {
29- result .push (... flattenTree (node .children || [], depth + 1 ))
30- }
14+ const skillRootId = ref (' ' )
15+ const viewerKey = ref (0 )
16+ const transport = new WebSocketTransport ()
17+ const initialViewState = { expandedPaths: [], selectedPath: ' SKILL.md' }
18+
19+ function open(name : string , badge : string , rootId : string ) {
20+ if (! rootId ) {
21+ show (' Missing skill file root id' , ' error' )
22+ return
3123 }
32- return result
33- }
34-
35- const flatTree = computed (() => flattenTree (tree .value ))
36-
37- function toggleDir(p : string ) {
38- if (collapsed .value .has (p )) collapsed .value .delete (p )
39- else collapsed .value .add (p )
40- collapsed .value = new Set (collapsed .value )
41- }
42-
43- // ── File viewer ──────────────────────────────────────────────────
44- const selectedPath = ref (' ' )
45- const fileContent = ref (' ' )
46- const fileLoading = ref (false )
47- const treeLoading = ref (false )
48-
49- async function selectFile(p : string ) {
50- if (selectedPath .value === p ) return
51- selectedPath .value = p
52- fileLoading .value = true
53- fileContent .value = ' '
54- try {
55- const res = await apiFetch (` ${skillApiBase .value }/${encodeURIComponent (skillName .value )}/file?path=${encodeURIComponent (p )} ` )
56- fileContent .value = res .data ?.content ?? ' '
57- } catch (e : any ) {
58- show (e .message , ' error' )
59- } finally {
60- fileLoading .value = false
61- }
62- }
63-
64- // ── Public API ───────────────────────────────────────────────────
65- async function open(name : string , badge : string , apiBase = ' /api/skills' ) {
6624 skillName .value = name
6725 skillBadge .value = badge
68- skillApiBase .value = apiBase
69- tree .value = []
70- selectedPath .value = ' '
71- fileContent .value = ' '
72- collapsed .value = new Set ()
73- treeLoading .value = true
26+ skillRootId .value = rootId
27+ viewerKey .value += 1
7428 visible .value = true
75-
76- try {
77- const res = await apiFetch (` ${apiBase }/${encodeURIComponent (name )}/tree ` )
78- tree .value = res .data || []
79- // Auto-select SKILL.md
80- const skillMd = flattenTree (tree .value ).find (({ node }) => node .type === ' file' && node .name === ' SKILL.md' )
81- if (skillMd ) {
82- selectFile (skillMd .node .path )
83- }
84- } catch (e : any ) {
85- show (e .message , ' error' )
86- } finally {
87- treeLoading .value = false
88- }
8929}
9030
9131function close() {
@@ -105,36 +45,12 @@ defineExpose({ open })
10545 </template >
10646
10747 <div class =" skill-viewer-body" >
108- <!-- Left: file tree -->
109- <STree :header =" t (' common.files' )" class="skill-tree">
110- <div v-if =" treeLoading" class =" skill-tree-loading" >{{ t('common.loading') }}</div >
111- <template v-else >
112- <STreeNode
113- v-for =" { node , depth } in flatTree "
114- :key =" node .path "
115- :level =" depth "
116- :type =" node .type "
117- :selected =" selectedPath === node .path "
118- :expandable =" node .type === ' dir' "
119- :expanded =" node .type === ' dir' && ! collapsed .has (node .path )"
120- @click =" node .type === ' dir' ? toggleDir (node .path ) : selectFile (node .path )"
121- >
122- {{ node.name }}
123- </STreeNode >
124- </template >
125- </STree >
126-
127- <!-- Right: file content -->
128- <div class =" skill-content" >
129- <template v-if =" selectedPath " >
130- <div class =" skill-content-toolbar" >
131- <span class =" skill-file-path" >{{ selectedPath }}</span >
132- </div >
133- <div v-if =" fileLoading" class =" skill-content-loading" >{{ t('common.loading') }}</div >
134- <pre v-else class =" skill-content-pre" >{{ fileContent }}</pre >
135- </template >
136- <div v-else class =" skill-content-empty" >{{ t('skills.select_file') }}</div >
137- </div >
48+ <FileExplorer
49+ :key =" viewerKey "
50+ :transport =" transport "
51+ :root-id =" skillRootId "
52+ :view-state =" initialViewState "
53+ />
13854 </div >
13955
14056 <template #footer >
@@ -145,78 +61,8 @@ defineExpose({ open })
14561
14662<style scoped>
14763.skill-viewer-body {
148- display : flex ;
14964 height : 70vh ;
15065 overflow : hidden ;
15166 margin : calc (-1 * var (--sui-sp-7 )) calc (-1 * var (--sui-sp-8 ));
15267}
153- .skill-tree {
154- width : 210px ;
155- flex-shrink : 0 ;
156- }
157- .skill-tree-loading {
158- padding : 20px ;
159- text-align : center ;
160- color : var (--sui-fg-disabled );
161- font-size : var (--sui-fs-md );
162- }
163-
164- .skill-content {
165- flex : 1 ;
166- display : flex ;
167- flex-direction : column ;
168- overflow : hidden ;
169- }
170- .skill-content-toolbar {
171- display : flex ;
172- align-items : center ;
173- gap : var (--sui-sp-3 );
174- padding : var (--sui-sp-3 ) var (--sui-sp-4 );
175- border-bottom : 1px solid var (--sui-border );
176- flex-shrink : 0 ;
177- background : var (--sui-bg );
178- }
179- .skill-file-path {
180- font-family : var (--sui-font-mono );
181- font-size : var (--sui-fs-sm );
182- color : var (--sui-fg-secondary );
183- }
184- .skill-content-loading {
185- flex : 1 ;
186- display : flex ;
187- align-items : center ;
188- justify-content : center ;
189- color : var (--sui-fg-disabled );
190- font-size : var (--sui-fs-md );
191- }
192- .skill-content-pre {
193- margin : 0 ;
194- padding : var (--sui-sp-4 ) var (--sui-sp-5 );
195- font-family : var (--sui-font-mono );
196- font-size : var (--sui-fs-sm );
197- line-height : 1.6 ;
198- white-space : pre-wrap ;
199- word-break : break-word ;
200- color : var (--sui-fg );
201- overflow : auto ;
202- flex : 1 ;
203- }
204- .skill-content-empty {
205- flex : 1 ;
206- display : flex ;
207- align-items : center ;
208- justify-content : center ;
209- color : var (--sui-fg-disabled );
210- font-size : var (--sui-fs-md );
211- }
212-
213- @media (max-width : 768px ) {
214- .skill-viewer-body { flex-direction : column ; }
215- .skill-tree {
216- width : 100% ;
217- max-height : 180px ;
218- border-right : none ;
219- border-bottom : 1px solid var (--sui-border );
220- }
221- }
22268 </style >
0 commit comments