Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
2fb3e265
authored
Aug 09, 2025
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: enhance TokensPage and useTokensData to support Fluent integration and notifications
parent
a08e027b
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
246 additions
and
5 deletions
+246
-5
web/src/components/table/tokens/index.jsx
+241
-4
web/src/hooks/tokens/useTokensData.js
+5
-1
No files found.
web/src/components/table/tokens/index.jsx
View file @
2fb3e265
...
...
@@ -17,7 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
React
from
'react'
;
import
React
,
{
useEffect
,
useRef
,
useState
}
from
'react'
;
import
{
Notification
,
Button
,
Space
,
Toast
,
Typography
,
Select
}
from
'@douyinfe/semi-ui'
;
import
{
API
,
showError
,
getModelCategories
,
selectFilter
}
from
'../../../helpers'
;
import
CardPro
from
'../../common/ui/CardPro'
;
import
TokensTable
from
'./TokensTable.jsx'
;
import
TokensActions
from
'./TokensActions.jsx'
;
...
...
@@ -28,9 +30,243 @@ import { useTokensData } from '../../../hooks/tokens/useTokensData';
import
{
useIsMobile
}
from
'../../../hooks/common/useIsMobile'
;
import
{
createCardProPagination
}
from
'../../../helpers/utils'
;
const
TokensPage
=
()
=>
{
const
tokensData
=
useTokensData
();
function
TokensPage
()
{
// Define the function first, then pass it into the hook to avoid TDZ errors
const
openFluentNotificationRef
=
useRef
(
null
);
const
tokensData
=
useTokensData
((
key
)
=>
openFluentNotificationRef
.
current
?.(
key
));
const
isMobile
=
useIsMobile
();
const
latestRef
=
useRef
({
tokens
:
[],
selectedKeys
:
[],
t
:
(
k
)
=>
k
,
selectedModel
:
''
,
prefillKey
:
''
});
const
[
modelOptions
,
setModelOptions
]
=
useState
([]);
const
[
selectedModel
,
setSelectedModel
]
=
useState
(
''
);
const
[
fluentNoticeOpen
,
setFluentNoticeOpen
]
=
useState
(
false
);
const
[
prefillKey
,
setPrefillKey
]
=
useState
(
''
);
// Keep latest data for handlers inside notifications
useEffect
(()
=>
{
latestRef
.
current
=
{
tokens
:
tokensData
.
tokens
,
selectedKeys
:
tokensData
.
selectedKeys
,
t
:
tokensData
.
t
,
selectedModel
,
prefillKey
,
};
},
[
tokensData
.
tokens
,
tokensData
.
selectedKeys
,
tokensData
.
t
,
selectedModel
,
prefillKey
]);
const
loadModels
=
async
()
=>
{
try
{
const
res
=
await
API
.
get
(
'/api/user/models'
);
const
{
success
,
message
,
data
}
=
res
.
data
||
{};
if
(
success
)
{
const
categories
=
getModelCategories
(
tokensData
.
t
);
const
options
=
(
data
||
[]).
map
((
model
)
=>
{
let
icon
=
null
;
for
(
const
[
key
,
category
]
of
Object
.
entries
(
categories
))
{
if
(
key
!==
'all'
&&
category
.
filter
({
model_name
:
model
}))
{
icon
=
category
.
icon
;
break
;
}
}
return
{
label
:
(
<
span
className=
"flex items-center gap-1"
>
{
icon
}
{
model
}
</
span
>
),
value
:
model
,
};
});
setModelOptions
(
options
);
}
else
{
showError
(
tokensData
.
t
(
message
));
}
}
catch
(
e
)
{
showError
(
e
.
message
||
'Failed to load models'
);
}
};
function
openFluentNotification
(
key
)
{
const
{
t
}
=
latestRef
.
current
;
const
SUPPRESS_KEY
=
'fluent_notify_suppressed'
;
if
(
localStorage
.
getItem
(
SUPPRESS_KEY
)
===
'1'
)
return
;
const
container
=
document
.
getElementById
(
'fluent-new-api-container'
);
if
(
!
container
)
{
Toast
.
warning
(
t
(
'未检测到 Fluent 容器,请确认扩展已启用'
));
return
;
}
setPrefillKey
(
key
||
''
);
setFluentNoticeOpen
(
true
);
if
(
modelOptions
.
length
===
0
)
{
// fire-and-forget; a later effect will refresh the notice content
loadModels
()
}
Notification
.
info
({
id
:
'fluent-detected'
,
title
:
t
(
'检测到 Fluent(流畅阅读)'
),
content
:
(
<
div
>
<
div
style=
{
{
marginBottom
:
8
}
}
>
{
prefillKey
?
t
(
'已检测到 Fluent 扩展,已从操作中指定密钥,将使用该密钥进行填充。请选择模型后继续。'
)
:
t
(
'已检测到 Fluent 扩展,请选择模型后可一键填充当前选中令牌(或本页第一个令牌)。'
)
}
</
div
>
<
div
style=
{
{
marginBottom
:
8
}
}
>
<
Select
placeholder=
{
t
(
'请选择模型'
)
}
optionList=
{
modelOptions
}
onChange=
{
setSelectedModel
}
filter=
{
selectFilter
}
style=
{
{
width
:
320
}
}
showClear
searchable
emptyContent=
{
t
(
'暂无数据'
)
}
/>
</
div
>
<
Space
>
<
Button
theme=
"solid"
type=
"primary"
onClick=
{
handlePrefillToFluent
}
>
{
t
(
'一键填充到 Fluent'
)
}
</
Button
>
<
Button
type=
"warning"
onClick=
{
()
=>
{
localStorage
.
setItem
(
SUPPRESS_KEY
,
'1'
);
Notification
.
close
(
'fluent-detected'
);
Toast
.
info
(
t
(
'已关闭后续提醒'
));
}
}
>
{
t
(
'不再提醒'
)
}
</
Button
>
<
Button
type=
"tertiary"
onClick=
{
()
=>
Notification
.
close
(
'fluent-detected'
)
}
>
{
t
(
'关闭'
)
}
</
Button
>
</
Space
>
</
div
>
),
duration
:
0
,
});
}
// assign after definition so hook callback can call it safely
openFluentNotificationRef
.
current
=
openFluentNotification
;
// Prefill to Fluent handler
const
handlePrefillToFluent
=
()
=>
{
const
{
tokens
,
selectedKeys
,
t
,
selectedModel
:
chosenModel
,
prefillKey
:
overrideKey
}
=
latestRef
.
current
;
const
container
=
document
.
getElementById
(
'fluent-new-api-container'
);
if
(
!
container
)
{
Toast
.
error
(
t
(
'未检测到 Fluent 容器'
));
return
;
}
if
(
!
chosenModel
)
{
Toast
.
warning
(
t
(
'请选择模型'
));
return
;
}
let
status
=
localStorage
.
getItem
(
'status'
);
let
serverAddress
=
''
;
if
(
status
)
{
try
{
status
=
JSON
.
parse
(
status
);
serverAddress
=
status
.
server_address
||
''
;
}
catch
(
_
)
{
}
}
if
(
!
serverAddress
)
serverAddress
=
window
.
location
.
origin
;
let
apiKeyToUse
=
''
;
if
(
overrideKey
)
{
apiKeyToUse
=
'sk-'
+
overrideKey
;
}
else
{
const
token
=
(
selectedKeys
&&
selectedKeys
.
length
===
1
)
?
selectedKeys
[
0
]
:
(
tokens
&&
tokens
.
length
>
0
?
tokens
[
0
]
:
null
);
if
(
!
token
)
{
Toast
.
warning
(
t
(
'没有可用令牌用于填充'
));
return
;
}
apiKeyToUse
=
'sk-'
+
token
.
key
;
}
const
payload
=
{
id
:
'new-api'
,
baseUrl
:
serverAddress
,
apiKey
:
apiKeyToUse
,
model
:
chosenModel
,
};
container
.
dispatchEvent
(
new
CustomEvent
(
'fluent:prefill'
,
{
detail
:
payload
}));
Toast
.
success
(
t
(
'已发送到 Fluent'
));
Notification
.
close
(
'fluent-detected'
);
};
// Show notification when Fluent container is available
useEffect
(()
=>
{
const
onAppeared
=
()
=>
{
openFluentNotification
();
};
const
onRemoved
=
()
=>
{
setFluentNoticeOpen
(
false
);
Notification
.
close
(
'fluent-detected'
);
};
window
.
addEventListener
(
'fluent-container:appeared'
,
onAppeared
);
window
.
addEventListener
(
'fluent-container:removed'
,
onRemoved
);
return
()
=>
{
window
.
removeEventListener
(
'fluent-container:appeared'
,
onAppeared
);
window
.
removeEventListener
(
'fluent-container:removed'
,
onRemoved
);
};
},
[]);
// When modelOptions or language changes while the notice is open, refresh the content
useEffect
(()
=>
{
if
(
fluentNoticeOpen
)
{
openFluentNotification
();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
modelOptions
,
selectedModel
,
tokensData
.
t
,
fluentNoticeOpen
]);
useEffect
(()
=>
{
const
selector
=
'#fluent-new-api-container'
;
const
root
=
document
.
body
||
document
.
documentElement
;
const
existing
=
document
.
querySelector
(
selector
);
if
(
existing
)
{
console
.
log
(
'Fluent container detected (initial):'
,
existing
);
window
.
dispatchEvent
(
new
CustomEvent
(
'fluent-container:appeared'
,
{
detail
:
existing
}));
}
const
isOrContainsTarget
=
(
node
)
=>
{
if
(
!
(
node
&&
node
.
nodeType
===
1
))
return
false
;
if
(
node
.
id
===
'fluent-new-api-container'
)
return
true
;
return
typeof
node
.
querySelector
===
'function'
&&
!!
node
.
querySelector
(
selector
);
};
const
observer
=
new
MutationObserver
((
mutations
)
=>
{
for
(
const
m
of
mutations
)
{
// appeared
for
(
const
added
of
m
.
addedNodes
)
{
if
(
isOrContainsTarget
(
added
))
{
const
el
=
document
.
querySelector
(
selector
);
if
(
el
)
{
console
.
log
(
'Fluent container appeared:'
,
el
);
window
.
dispatchEvent
(
new
CustomEvent
(
'fluent-container:appeared'
,
{
detail
:
el
}));
}
break
;
}
}
// removed
for
(
const
removed
of
m
.
removedNodes
)
{
if
(
isOrContainsTarget
(
removed
))
{
const
elNow
=
document
.
querySelector
(
selector
);
if
(
!
elNow
)
{
console
.
log
(
'Fluent container removed'
);
window
.
dispatchEvent
(
new
CustomEvent
(
'fluent-container:removed'
));
}
break
;
}
}
}
});
observer
.
observe
(
root
,
{
childList
:
true
,
subtree
:
true
});
return
()
=>
observer
.
disconnect
();
},
[]);
const
{
// Edit state
...
...
@@ -119,6 +355,6 @@ const TokensPage = () => {
</
CardPro
>
</>
);
}
;
}
export
default
TokensPage
;
\ No newline at end of file
web/src/hooks/tokens/useTokensData.js
View file @
2fb3e265
...
...
@@ -29,7 +29,7 @@ import {
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
{
useTableCompactMode
}
from
'../common/useTableCompactMode'
;
export
const
useTokensData
=
()
=>
{
export
const
useTokensData
=
(
openFluentNotification
)
=>
{
const
{
t
}
=
useTranslation
();
// Basic state
...
...
@@ -121,6 +121,10 @@ export const useTokensData = () => {
// Open link function for chat integrations
const
onOpenLink
=
async
(
type
,
url
,
record
)
=>
{
if
(
url
&&
url
.
startsWith
(
'fluent'
))
{
openFluentNotification
(
record
.
key
);
return
;
}
let
status
=
localStorage
.
getItem
(
'status'
);
let
serverAddress
=
''
;
if
(
status
)
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment