@yh-ui/hooks 0.1.10 → 0.1.12
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.
- package/dist/index.cjs +623 -12
- package/dist/index.d.cts +333 -7
- package/dist/index.d.mts +333 -7
- package/dist/index.d.ts +333 -7
- package/dist/index.mjs +615 -13
- package/dist/use-ai/index.cjs +38 -0
- package/dist/use-ai/index.d.ts +3 -0
- package/dist/use-ai/index.mjs +3 -0
- package/dist/use-ai/use-ai-chat.cjs +193 -0
- package/dist/use-ai/use-ai-chat.d.ts +115 -0
- package/dist/use-ai/use-ai-chat.mjs +182 -0
- package/dist/use-ai/use-ai-conversations.cjs +254 -0
- package/dist/use-ai/use-ai-conversations.d.ts +148 -0
- package/dist/use-ai/use-ai-conversations.mjs +241 -0
- package/dist/use-ai/use-ai-stream.cjs +190 -0
- package/dist/use-ai/use-ai-stream.d.ts +64 -0
- package/dist/use-ai/use-ai-stream.mjs +172 -0
- package/dist/use-form-item/index.d.ts +1 -1
- package/dist/use-locale/dayjs-locale.cjs +19 -12
- package/dist/use-locale/dayjs-locale.d.ts +1 -8
- package/dist/use-locale/dayjs-locale.mjs +20 -12
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -4,10 +4,6 @@ const vue = require('vue');
|
|
|
4
4
|
const locale = require('@yh-ui/locale');
|
|
5
5
|
const _dayjs = require('dayjs');
|
|
6
6
|
require('dayjs/locale/en');
|
|
7
|
-
require('dayjs/locale/zh-cn');
|
|
8
|
-
require('dayjs/locale/zh-tw');
|
|
9
|
-
require('dayjs/locale/ja');
|
|
10
|
-
require('dayjs/locale/ko');
|
|
11
7
|
|
|
12
8
|
function _interopNamespaceCompat(e) {
|
|
13
9
|
if (e && typeof e === 'object' && 'default' in e) return e;
|
|
@@ -161,13 +157,16 @@ const useConfig = () => {
|
|
|
161
157
|
};
|
|
162
158
|
|
|
163
159
|
const dayjs = _dayjs__namespace.default || _dayjs__namespace;
|
|
164
|
-
const
|
|
160
|
+
const dayjsLocales = undefined(
|
|
161
|
+
["../../../../node_modules/dayjs/locale/*.js", "!../../../../node_modules/dayjs/locale/en.js"],
|
|
162
|
+
{ eager: false }
|
|
163
|
+
);
|
|
164
|
+
const loadedLocales = /* @__PURE__ */ new Set(["en"]);
|
|
165
165
|
const localeMapping = {
|
|
166
166
|
"zh-cn": "zh-cn",
|
|
167
167
|
"zh-tw": "zh-tw",
|
|
168
168
|
"zh-hk": "zh-hk",
|
|
169
169
|
"zh-mo": "zh-tw",
|
|
170
|
-
// 澳门使用繁体
|
|
171
170
|
en: "en",
|
|
172
171
|
ja: "ja",
|
|
173
172
|
ko: "ko",
|
|
@@ -241,12 +240,21 @@ const setDayjsLocale = async (localeCode) => {
|
|
|
241
240
|
dayjs.locale(dayjsLocale);
|
|
242
241
|
return;
|
|
243
242
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
243
|
+
if (dayjsLocale === "en") {
|
|
244
|
+
dayjs.locale("en");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const path = `../../../../node_modules/dayjs/locale/${dayjsLocale}.js`;
|
|
248
|
+
const loader = dayjsLocales[path];
|
|
249
|
+
if (loader) {
|
|
250
|
+
try {
|
|
251
|
+
await loader();
|
|
252
|
+
loadedLocales.add(dayjsLocale);
|
|
253
|
+
dayjs.locale(dayjsLocale);
|
|
254
|
+
} catch {
|
|
255
|
+
dayjs.locale("en");
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
250
258
|
dayjs.locale("en");
|
|
251
259
|
}
|
|
252
260
|
};
|
|
@@ -555,19 +563,622 @@ function useClickOutside(target, handler) {
|
|
|
555
563
|
useEventListener(window, "touchstart", listener, true);
|
|
556
564
|
}
|
|
557
565
|
|
|
566
|
+
const openaiParser = (raw) => {
|
|
567
|
+
const lines = raw.split("\n");
|
|
568
|
+
let text = "";
|
|
569
|
+
for (const line of lines) {
|
|
570
|
+
if (!line.startsWith("data: ")) continue;
|
|
571
|
+
const data = line.slice(6).trim();
|
|
572
|
+
if (data === "[DONE]") break;
|
|
573
|
+
try {
|
|
574
|
+
const json = JSON.parse(data);
|
|
575
|
+
const delta = json?.choices?.[0]?.delta?.content;
|
|
576
|
+
if (delta) text += delta;
|
|
577
|
+
} catch {
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return text || null;
|
|
581
|
+
};
|
|
582
|
+
const ernieParser = (raw) => {
|
|
583
|
+
const lines = raw.split("\n");
|
|
584
|
+
let text = "";
|
|
585
|
+
for (const line of lines) {
|
|
586
|
+
if (!line.startsWith("data: ")) continue;
|
|
587
|
+
const data = line.slice(6).trim();
|
|
588
|
+
try {
|
|
589
|
+
const json = JSON.parse(data);
|
|
590
|
+
if (json?.result) text += json.result;
|
|
591
|
+
} catch {
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return text || null;
|
|
595
|
+
};
|
|
596
|
+
const qwenParser = (raw) => {
|
|
597
|
+
const lines = raw.split("\n");
|
|
598
|
+
let text = "";
|
|
599
|
+
for (const line of lines) {
|
|
600
|
+
if (!line.startsWith("data: ")) continue;
|
|
601
|
+
const data = line.slice(6).trim();
|
|
602
|
+
try {
|
|
603
|
+
const json = JSON.parse(data);
|
|
604
|
+
const t = json?.output?.text;
|
|
605
|
+
if (t) text += t;
|
|
606
|
+
} catch {
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return text || null;
|
|
610
|
+
};
|
|
611
|
+
const plainTextParser = (raw) => raw || null;
|
|
612
|
+
class TypewriterThrottle {
|
|
613
|
+
queue = [];
|
|
614
|
+
rafId = null;
|
|
615
|
+
onUpdate;
|
|
616
|
+
charsPerFrame;
|
|
617
|
+
constructor(onUpdate, charsPerFrame = 3) {
|
|
618
|
+
this.onUpdate = onUpdate;
|
|
619
|
+
this.charsPerFrame = charsPerFrame;
|
|
620
|
+
}
|
|
621
|
+
push(text) {
|
|
622
|
+
this.queue.push(...text.split(""));
|
|
623
|
+
if (this.rafId === null) {
|
|
624
|
+
this.schedule();
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
schedule() {
|
|
628
|
+
this.rafId = requestAnimationFrame(() => {
|
|
629
|
+
this.rafId = null;
|
|
630
|
+
if (this.queue.length === 0) return;
|
|
631
|
+
const batch = this.queue.splice(0, this.charsPerFrame).join("");
|
|
632
|
+
this.onUpdate(batch);
|
|
633
|
+
if (this.queue.length > 0) {
|
|
634
|
+
this.schedule();
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
flush() {
|
|
639
|
+
if (this.rafId !== null) {
|
|
640
|
+
cancelAnimationFrame(this.rafId);
|
|
641
|
+
this.rafId = null;
|
|
642
|
+
}
|
|
643
|
+
if (this.queue.length > 0) {
|
|
644
|
+
const remaining = this.queue.splice(0).join("");
|
|
645
|
+
this.onUpdate(remaining);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
cancel() {
|
|
649
|
+
if (this.rafId !== null) {
|
|
650
|
+
cancelAnimationFrame(this.rafId);
|
|
651
|
+
this.rafId = null;
|
|
652
|
+
}
|
|
653
|
+
this.queue = [];
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function useAiStream(options) {
|
|
657
|
+
const isStreaming = vue.ref(false);
|
|
658
|
+
const currentContent = vue.ref("");
|
|
659
|
+
let abortController = new AbortController();
|
|
660
|
+
let typewriter = null;
|
|
661
|
+
const parser = options.parser ?? plainTextParser;
|
|
662
|
+
const enableTypewriter = options.typewriter !== false;
|
|
663
|
+
const charsPerFrame = options.charsPerFrame ?? 3;
|
|
664
|
+
const stop = () => {
|
|
665
|
+
if (isStreaming.value) {
|
|
666
|
+
abortController.abort();
|
|
667
|
+
isStreaming.value = false;
|
|
668
|
+
typewriter?.flush();
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
const fetchStream = async (query, ...args) => {
|
|
672
|
+
isStreaming.value = true;
|
|
673
|
+
currentContent.value = "";
|
|
674
|
+
abortController = new AbortController();
|
|
675
|
+
if (enableTypewriter) {
|
|
676
|
+
typewriter = new TypewriterThrottle((chunk) => {
|
|
677
|
+
currentContent.value += chunk;
|
|
678
|
+
options.onUpdate?.(chunk, currentContent.value);
|
|
679
|
+
}, charsPerFrame);
|
|
680
|
+
}
|
|
681
|
+
const pushText = (text) => {
|
|
682
|
+
if (!text) return;
|
|
683
|
+
if (enableTypewriter && typewriter) {
|
|
684
|
+
typewriter.push(text);
|
|
685
|
+
} else {
|
|
686
|
+
currentContent.value += text;
|
|
687
|
+
options.onUpdate?.(text, currentContent.value);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
try {
|
|
691
|
+
const response = await options.request(query, ...args);
|
|
692
|
+
if (typeof response === "object" && response !== null && Symbol.asyncIterator in response) {
|
|
693
|
+
for await (const chunk of response) {
|
|
694
|
+
if (abortController.signal.aborted) break;
|
|
695
|
+
const parsed = parser(chunk);
|
|
696
|
+
if (parsed) pushText(parsed);
|
|
697
|
+
}
|
|
698
|
+
} else if (response instanceof Response && response.body) {
|
|
699
|
+
const reader = response.body.getReader();
|
|
700
|
+
const decoder = new TextDecoder("utf-8");
|
|
701
|
+
while (true) {
|
|
702
|
+
if (abortController.signal.aborted) {
|
|
703
|
+
reader.cancel();
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
const { done, value } = await reader.read();
|
|
707
|
+
if (done) break;
|
|
708
|
+
const chunkStr = decoder.decode(value, { stream: true });
|
|
709
|
+
const parsed = parser(chunkStr);
|
|
710
|
+
if (parsed) pushText(parsed);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (!abortController.signal.aborted) {
|
|
714
|
+
if (enableTypewriter && typewriter) {
|
|
715
|
+
typewriter.flush();
|
|
716
|
+
}
|
|
717
|
+
isStreaming.value = false;
|
|
718
|
+
options.onFinish?.(currentContent.value);
|
|
719
|
+
}
|
|
720
|
+
} catch (e) {
|
|
721
|
+
if (e.name !== "AbortError") {
|
|
722
|
+
options.onError?.(e);
|
|
723
|
+
}
|
|
724
|
+
typewriter?.cancel();
|
|
725
|
+
isStreaming.value = false;
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
return {
|
|
729
|
+
isStreaming,
|
|
730
|
+
currentContent,
|
|
731
|
+
fetchStream,
|
|
732
|
+
stop,
|
|
733
|
+
// 暴露解析器供测试/自定义使用
|
|
734
|
+
parsers: { openaiParser, ernieParser, qwenParser, plainTextParser }
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function createTypewriter(onChar, charsPerFrame) {
|
|
739
|
+
const queue = [];
|
|
740
|
+
let rafId = null;
|
|
741
|
+
const schedule = () => {
|
|
742
|
+
rafId = requestAnimationFrame(() => {
|
|
743
|
+
rafId = null;
|
|
744
|
+
if (queue.length === 0) return;
|
|
745
|
+
const batch = queue.splice(0, charsPerFrame).join("");
|
|
746
|
+
onChar(batch);
|
|
747
|
+
if (queue.length > 0) schedule();
|
|
748
|
+
});
|
|
749
|
+
};
|
|
750
|
+
return {
|
|
751
|
+
push(text) {
|
|
752
|
+
queue.push(...text.split(""));
|
|
753
|
+
if (rafId === null) schedule();
|
|
754
|
+
},
|
|
755
|
+
flush() {
|
|
756
|
+
if (rafId !== null) {
|
|
757
|
+
cancelAnimationFrame(rafId);
|
|
758
|
+
rafId = null;
|
|
759
|
+
}
|
|
760
|
+
if (queue.length > 0) {
|
|
761
|
+
onChar(queue.splice(0).join(""));
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
cancel() {
|
|
765
|
+
if (rafId !== null) {
|
|
766
|
+
cancelAnimationFrame(rafId);
|
|
767
|
+
rafId = null;
|
|
768
|
+
}
|
|
769
|
+
queue.length = 0;
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function useAiChat(options = {}) {
|
|
774
|
+
const {
|
|
775
|
+
idGenerator = () => Math.random().toString(36).substring(2, 9),
|
|
776
|
+
parser = plainTextParser,
|
|
777
|
+
typewriter: enableTypewriter = true,
|
|
778
|
+
charsPerFrame = 3,
|
|
779
|
+
systemPrompt
|
|
780
|
+
} = options;
|
|
781
|
+
const messages = vue.ref(options.initialMessages ?? []);
|
|
782
|
+
const isGenerating = vue.ref(false);
|
|
783
|
+
const isSending = vue.computed(() => isGenerating.value);
|
|
784
|
+
let abortController = null;
|
|
785
|
+
const stop = () => {
|
|
786
|
+
if (abortController && isGenerating.value) {
|
|
787
|
+
abortController.abort();
|
|
788
|
+
isGenerating.value = false;
|
|
789
|
+
const lastMsg = messages.value[messages.value.length - 1];
|
|
790
|
+
if (lastMsg?.role === "assistant" && lastMsg.status === "generating") {
|
|
791
|
+
lastMsg.status = "stopped";
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
const clear = () => {
|
|
796
|
+
stop();
|
|
797
|
+
messages.value = [];
|
|
798
|
+
};
|
|
799
|
+
const removeMessage = (id) => {
|
|
800
|
+
const idx = messages.value.findIndex((m) => m.id === id);
|
|
801
|
+
if (idx !== -1) messages.value.splice(idx, 1);
|
|
802
|
+
};
|
|
803
|
+
const updateMessage = (id, patch) => {
|
|
804
|
+
const msg = messages.value.find((m) => m.id === id);
|
|
805
|
+
if (msg) Object.assign(msg, patch);
|
|
806
|
+
};
|
|
807
|
+
const sendMessage = async (content) => {
|
|
808
|
+
if (!content.trim() || isGenerating.value) return;
|
|
809
|
+
messages.value.push({
|
|
810
|
+
id: idGenerator(),
|
|
811
|
+
role: "user",
|
|
812
|
+
content,
|
|
813
|
+
createAt: Date.now(),
|
|
814
|
+
status: "success"
|
|
815
|
+
});
|
|
816
|
+
if (!options.request) return;
|
|
817
|
+
const assId = idGenerator();
|
|
818
|
+
const assistantMsg = {
|
|
819
|
+
id: assId,
|
|
820
|
+
role: "assistant",
|
|
821
|
+
content: "",
|
|
822
|
+
createAt: Date.now(),
|
|
823
|
+
status: "loading"
|
|
824
|
+
};
|
|
825
|
+
messages.value.push(assistantMsg);
|
|
826
|
+
isGenerating.value = true;
|
|
827
|
+
abortController = new AbortController();
|
|
828
|
+
const history = [];
|
|
829
|
+
if (systemPrompt) {
|
|
830
|
+
history.push({
|
|
831
|
+
id: "system",
|
|
832
|
+
role: "system",
|
|
833
|
+
content: systemPrompt,
|
|
834
|
+
createAt: 0,
|
|
835
|
+
status: "success"
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
history.push(...messages.value.slice(0, -2));
|
|
839
|
+
try {
|
|
840
|
+
const response = await options.request(content, history, abortController.signal);
|
|
841
|
+
const targetMsg = messages.value.find((m) => m.id === assId);
|
|
842
|
+
targetMsg.status = "generating";
|
|
843
|
+
let typewriterInstance = null;
|
|
844
|
+
if (enableTypewriter && typeof requestAnimationFrame !== "undefined") {
|
|
845
|
+
typewriterInstance = createTypewriter((chars) => {
|
|
846
|
+
targetMsg.content += chars;
|
|
847
|
+
}, charsPerFrame);
|
|
848
|
+
}
|
|
849
|
+
const pushChunk = (text) => {
|
|
850
|
+
if (!text) return;
|
|
851
|
+
if (typewriterInstance) {
|
|
852
|
+
typewriterInstance.push(text);
|
|
853
|
+
} else {
|
|
854
|
+
targetMsg.content += text;
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
if (typeof response === "object" && response !== null && Symbol.asyncIterator in response) {
|
|
858
|
+
for await (const chunk of response) {
|
|
859
|
+
if (abortController.signal.aborted) break;
|
|
860
|
+
const parsed = parser(chunk);
|
|
861
|
+
if (parsed) pushChunk(parsed);
|
|
862
|
+
}
|
|
863
|
+
} else if (response instanceof Response && response.body) {
|
|
864
|
+
const reader = response.body.getReader();
|
|
865
|
+
const decoder = new TextDecoder("utf-8");
|
|
866
|
+
while (true) {
|
|
867
|
+
if (abortController.signal.aborted) {
|
|
868
|
+
reader.cancel();
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
const { done, value } = await reader.read();
|
|
872
|
+
if (done) break;
|
|
873
|
+
const chunkStr = decoder.decode(value, { stream: true });
|
|
874
|
+
const parsed = parser(chunkStr);
|
|
875
|
+
if (parsed) pushChunk(parsed);
|
|
876
|
+
}
|
|
877
|
+
} else if (typeof response === "string") {
|
|
878
|
+
pushChunk(response);
|
|
879
|
+
}
|
|
880
|
+
if (typewriterInstance) {
|
|
881
|
+
typewriterInstance.flush();
|
|
882
|
+
}
|
|
883
|
+
if (!abortController.signal.aborted) {
|
|
884
|
+
targetMsg.status = "success";
|
|
885
|
+
options.onFinish?.(targetMsg);
|
|
886
|
+
}
|
|
887
|
+
} catch (e) {
|
|
888
|
+
if (e.name !== "AbortError") {
|
|
889
|
+
const targetMsg = messages.value.find((m) => m.id === assId);
|
|
890
|
+
if (targetMsg) targetMsg.status = "error";
|
|
891
|
+
options.onError?.(e);
|
|
892
|
+
}
|
|
893
|
+
} finally {
|
|
894
|
+
if (isGenerating.value) {
|
|
895
|
+
isGenerating.value = false;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
return {
|
|
900
|
+
/** 会话历史 */
|
|
901
|
+
messages,
|
|
902
|
+
/** 是否正在生成(等同 isSending,别名友好) */
|
|
903
|
+
isGenerating,
|
|
904
|
+
/** 同 isGenerating,语义别名 */
|
|
905
|
+
isSending,
|
|
906
|
+
/** 触发发送(自动处理流、打字机) */
|
|
907
|
+
sendMessage,
|
|
908
|
+
/** 停止/中断当前生成 */
|
|
909
|
+
stop,
|
|
910
|
+
/** 移除单条消息 */
|
|
911
|
+
removeMessage,
|
|
912
|
+
/** 修改单条消息内容 */
|
|
913
|
+
updateMessage,
|
|
914
|
+
/** 重置清空所有会话 */
|
|
915
|
+
clear
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const localStorageAdapter = {
|
|
920
|
+
getItem: (key) => {
|
|
921
|
+
try {
|
|
922
|
+
return localStorage.getItem(key);
|
|
923
|
+
} catch {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
setItem: (key, value) => {
|
|
928
|
+
try {
|
|
929
|
+
localStorage.setItem(key, value);
|
|
930
|
+
} catch {
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
removeItem: (key) => {
|
|
934
|
+
try {
|
|
935
|
+
localStorage.removeItem(key);
|
|
936
|
+
} catch {
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
class IndexedDBAdapter {
|
|
941
|
+
db = null;
|
|
942
|
+
dbName;
|
|
943
|
+
storeName = "ai_conversations";
|
|
944
|
+
ready;
|
|
945
|
+
constructor(dbName = "yh-ui-ai") {
|
|
946
|
+
this.dbName = dbName;
|
|
947
|
+
this.ready = this.init();
|
|
948
|
+
}
|
|
949
|
+
init() {
|
|
950
|
+
return new Promise((resolve, reject) => {
|
|
951
|
+
if (typeof indexedDB === "undefined") {
|
|
952
|
+
resolve();
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const req = indexedDB.open(this.dbName, 1);
|
|
956
|
+
req.onupgradeneeded = () => {
|
|
957
|
+
req.result.createObjectStore(this.storeName);
|
|
958
|
+
};
|
|
959
|
+
req.onsuccess = () => {
|
|
960
|
+
this.db = req.result;
|
|
961
|
+
resolve();
|
|
962
|
+
};
|
|
963
|
+
req.onerror = () => reject(req.error);
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
async getItem(key) {
|
|
967
|
+
await this.ready;
|
|
968
|
+
if (!this.db) return null;
|
|
969
|
+
return new Promise((resolve) => {
|
|
970
|
+
const tx = this.db.transaction(this.storeName, "readonly");
|
|
971
|
+
const req = tx.objectStore(this.storeName).get(key);
|
|
972
|
+
req.onsuccess = () => resolve(req.result ?? null);
|
|
973
|
+
req.onerror = () => resolve(null);
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
async setItem(key, value) {
|
|
977
|
+
await this.ready;
|
|
978
|
+
if (!this.db) return;
|
|
979
|
+
return new Promise((resolve) => {
|
|
980
|
+
const tx = this.db.transaction(this.storeName, "readwrite");
|
|
981
|
+
tx.objectStore(this.storeName).put(value, key);
|
|
982
|
+
tx.oncomplete = () => resolve();
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
async removeItem(key) {
|
|
986
|
+
await this.ready;
|
|
987
|
+
if (!this.db) return;
|
|
988
|
+
return new Promise((resolve) => {
|
|
989
|
+
const tx = this.db.transaction(this.storeName, "readwrite");
|
|
990
|
+
tx.objectStore(this.storeName).delete(key);
|
|
991
|
+
tx.oncomplete = () => resolve();
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function getGroupLabel(updatedAt) {
|
|
996
|
+
const now = Date.now();
|
|
997
|
+
const diff = now - updatedAt;
|
|
998
|
+
const oneDay = 864e5;
|
|
999
|
+
if (diff < oneDay) return "today";
|
|
1000
|
+
if (diff < 7 * oneDay) return "last7Days";
|
|
1001
|
+
if (diff < 30 * oneDay) return "last30Days";
|
|
1002
|
+
return "earlier";
|
|
1003
|
+
}
|
|
1004
|
+
const GROUP_ORDER = ["today", "last7Days", "last30Days", "earlier"];
|
|
1005
|
+
function useAiConversations(options = {}) {
|
|
1006
|
+
const {
|
|
1007
|
+
idGenerator = () => Math.random().toString(36).substring(2, 9),
|
|
1008
|
+
storage = "localStorage",
|
|
1009
|
+
storageKey = "yh-ui-ai-conversations",
|
|
1010
|
+
pageSize = 20
|
|
1011
|
+
} = options;
|
|
1012
|
+
let adapter = null;
|
|
1013
|
+
if (storage === "localStorage") {
|
|
1014
|
+
adapter = localStorageAdapter;
|
|
1015
|
+
} else if (storage === "indexedDB") {
|
|
1016
|
+
adapter = new IndexedDBAdapter();
|
|
1017
|
+
} else if (storage && typeof storage === "object") {
|
|
1018
|
+
adapter = storage;
|
|
1019
|
+
}
|
|
1020
|
+
const conversations = vue.ref([]);
|
|
1021
|
+
const page = vue.ref(1);
|
|
1022
|
+
const isLoadingMore = vue.ref(false);
|
|
1023
|
+
const initPromise = (async () => {
|
|
1024
|
+
let stored = [];
|
|
1025
|
+
if (adapter) {
|
|
1026
|
+
try {
|
|
1027
|
+
const raw = await adapter.getItem(storageKey);
|
|
1028
|
+
if (raw) stored = JSON.parse(raw);
|
|
1029
|
+
} catch {
|
|
1030
|
+
stored = [];
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const init = options.initialConversations ?? [];
|
|
1034
|
+
const merged = [...init];
|
|
1035
|
+
for (const s of stored) {
|
|
1036
|
+
if (!merged.find((c) => c.id === s.id)) {
|
|
1037
|
+
merged.push(s);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
conversations.value = merged.sort((a, b) => {
|
|
1041
|
+
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
|
|
1042
|
+
return b.updatedAt - a.updatedAt;
|
|
1043
|
+
});
|
|
1044
|
+
})();
|
|
1045
|
+
const persist = async () => {
|
|
1046
|
+
if (!adapter) return;
|
|
1047
|
+
try {
|
|
1048
|
+
await adapter.setItem(storageKey, JSON.stringify(conversations.value));
|
|
1049
|
+
} catch {
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
const groupedConversations = vue.computed(() => {
|
|
1053
|
+
const groups = {
|
|
1054
|
+
today: [],
|
|
1055
|
+
last7Days: [],
|
|
1056
|
+
last30Days: [],
|
|
1057
|
+
earlier: []
|
|
1058
|
+
};
|
|
1059
|
+
for (const c of conversations.value) {
|
|
1060
|
+
if (c.pinned) continue;
|
|
1061
|
+
const key = getGroupLabel(c.updatedAt);
|
|
1062
|
+
groups[key].push(c);
|
|
1063
|
+
}
|
|
1064
|
+
const result = [];
|
|
1065
|
+
const pinned = conversations.value.filter((c) => c.pinned);
|
|
1066
|
+
if (pinned.length > 0) {
|
|
1067
|
+
result.push({ label: "pinned", items: pinned });
|
|
1068
|
+
}
|
|
1069
|
+
for (const key of GROUP_ORDER) {
|
|
1070
|
+
if (groups[key].length > 0) {
|
|
1071
|
+
result.push({ label: key, items: groups[key] });
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return result;
|
|
1075
|
+
});
|
|
1076
|
+
const pagedConversations = vue.computed(() => {
|
|
1077
|
+
return conversations.value.slice(0, page.value * pageSize);
|
|
1078
|
+
});
|
|
1079
|
+
const hasMore = vue.computed(() => {
|
|
1080
|
+
return pagedConversations.value.length < conversations.value.length;
|
|
1081
|
+
});
|
|
1082
|
+
const loadMore = async () => {
|
|
1083
|
+
if (!hasMore.value || isLoadingMore.value) return;
|
|
1084
|
+
isLoadingMore.value = true;
|
|
1085
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1086
|
+
page.value++;
|
|
1087
|
+
isLoadingMore.value = false;
|
|
1088
|
+
};
|
|
1089
|
+
const createConversation = async (title, meta) => {
|
|
1090
|
+
const newConv = {
|
|
1091
|
+
id: idGenerator(),
|
|
1092
|
+
title,
|
|
1093
|
+
updatedAt: Date.now(),
|
|
1094
|
+
meta
|
|
1095
|
+
};
|
|
1096
|
+
conversations.value.unshift(newConv);
|
|
1097
|
+
await persist();
|
|
1098
|
+
return newConv;
|
|
1099
|
+
};
|
|
1100
|
+
const removeConversation = async (id) => {
|
|
1101
|
+
const idx = conversations.value.findIndex((c) => c.id === id);
|
|
1102
|
+
if (idx !== -1) {
|
|
1103
|
+
conversations.value.splice(idx, 1);
|
|
1104
|
+
await persist();
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
const updateConversation = async (id, updates) => {
|
|
1108
|
+
const idx = conversations.value.findIndex((c) => c.id === id);
|
|
1109
|
+
if (idx !== -1) {
|
|
1110
|
+
conversations.value[idx] = {
|
|
1111
|
+
...conversations.value[idx],
|
|
1112
|
+
...updates,
|
|
1113
|
+
updatedAt: Date.now()
|
|
1114
|
+
};
|
|
1115
|
+
await persist();
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
const pinConversation = async (id, pinned = true) => {
|
|
1119
|
+
await updateConversation(id, { pinned });
|
|
1120
|
+
conversations.value.sort((a, b) => {
|
|
1121
|
+
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
|
|
1122
|
+
return b.updatedAt - a.updatedAt;
|
|
1123
|
+
});
|
|
1124
|
+
await persist();
|
|
1125
|
+
};
|
|
1126
|
+
const clear = async () => {
|
|
1127
|
+
conversations.value = [];
|
|
1128
|
+
if (adapter) {
|
|
1129
|
+
await adapter.removeItem(storageKey);
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
return {
|
|
1133
|
+
/** 完整会话列表 */
|
|
1134
|
+
conversations,
|
|
1135
|
+
/** 按时间分组后的列表(置顶 / 今天 / 最近 7 天 / 更早) */
|
|
1136
|
+
groupedConversations,
|
|
1137
|
+
/** 分页后的列表 */
|
|
1138
|
+
pagedConversations,
|
|
1139
|
+
/** 是否还有更多数据 */
|
|
1140
|
+
hasMore,
|
|
1141
|
+
/** 加载更多 */
|
|
1142
|
+
loadMore,
|
|
1143
|
+
/** 加载更多状态 */
|
|
1144
|
+
isLoadingMore,
|
|
1145
|
+
/** 等待初始化完成(SSR 场景使用) */
|
|
1146
|
+
ready: initPromise,
|
|
1147
|
+
/** 新建会话 */
|
|
1148
|
+
createConversation,
|
|
1149
|
+
/** 删除会话 */
|
|
1150
|
+
removeConversation,
|
|
1151
|
+
/** 更新会话属性 */
|
|
1152
|
+
updateConversation,
|
|
1153
|
+
/** 置顶/取消置顶 */
|
|
1154
|
+
pinConversation,
|
|
1155
|
+
/** 清空全部 */
|
|
1156
|
+
clear
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
558
1160
|
exports.FormContextKey = FormContextKey;
|
|
559
1161
|
exports.FormItemContextKey = FormItemContextKey;
|
|
1162
|
+
exports.IndexedDBAdapter = IndexedDBAdapter;
|
|
560
1163
|
exports.configProviderContextKey = configProviderContextKey;
|
|
561
1164
|
exports.createZIndexCounter = createZIndexCounter;
|
|
562
1165
|
exports.defaultNamespace = defaultNamespace;
|
|
1166
|
+
exports.ernieParser = ernieParser;
|
|
563
1167
|
exports.getDayjsLocale = getDayjsLocale;
|
|
564
1168
|
exports.getNextZIndex = getNextZIndex;
|
|
565
1169
|
exports.idInjectionKey = idInjectionKey;
|
|
1170
|
+
exports.localStorageAdapter = localStorageAdapter;
|
|
566
1171
|
exports.namespaceContextKey = namespaceContextKey;
|
|
1172
|
+
exports.openaiParser = openaiParser;
|
|
1173
|
+
exports.plainTextParser = plainTextParser;
|
|
1174
|
+
exports.qwenParser = qwenParser;
|
|
567
1175
|
exports.resetZIndex = resetZIndex;
|
|
568
1176
|
exports.setDayjsLocale = setDayjsLocale;
|
|
569
1177
|
exports.setDayjsLocaleSync = setDayjsLocaleSync;
|
|
570
1178
|
exports.updateDayjsMonths = updateDayjsMonths;
|
|
1179
|
+
exports.useAiChat = useAiChat;
|
|
1180
|
+
exports.useAiConversations = useAiConversations;
|
|
1181
|
+
exports.useAiStream = useAiStream;
|
|
571
1182
|
exports.useCache = useCache;
|
|
572
1183
|
exports.useClickOutside = useClickOutside;
|
|
573
1184
|
exports.useConfig = useConfig;
|