Compare commits

...

4 Commits

Author SHA1 Message Date
Arvin Xu 0cb7118eff Merge branch 'next' into feat/support-auto-hint 2025-11-29 13:49:25 +08:00
arvinxx d43781bc40 fix auto complete 2025-11-18 10:07:43 +08:00
arvinxx 1ef3246125 update delay 2025-11-17 10:09:07 +08:00
arvinxx 13ab9bfb24 support auto complete 2025-11-17 09:59:10 +08:00
9 changed files with 187 additions and 15 deletions
+1 -1
View File
@@ -169,7 +169,7 @@
"@lobehub/charts": "^2.1.2",
"@lobehub/chat-plugin-sdk": "^1.32.4",
"@lobehub/chat-plugins-gateway": "^1.9.0",
"@lobehub/editor": "^1.23.1",
"@lobehub/editor": "https://pkg.pr.new/lobehub/lobe-editor/@lobehub/editor@53",
"@lobehub/icons": "^2.43.1",
"@lobehub/market-sdk": "^0.23.0",
"@lobehub/tts": "^2.0.1",
+1
View File
@@ -1 +1,2 @@
export * from './inputAutoComplete';
export * from './supervisor';
@@ -0,0 +1,81 @@
import { ChatStreamPayload } from '@/types/index';
export interface AutoCompleteParams {
/**
* Optional conversation context
*/
context?: string[];
/**
* Current partial input from user
*/
input: string;
}
export const contextInputAutoComplete = ({
input,
context = [],
}: AutoCompleteParams): Partial<ChatStreamPayload> => {
const hasContext = context.length > 0;
return {
messages: [
{
content: `You predict what the user will TYPE next, like search box autocomplete.
Task: Complete the text with 3-8 words the user would naturally type next.
✅ CORRECT examples:
Input: "How to install"
Output: "Node.js on Mac"
Input: "请帮我"
Output: "写一个函数"
Input: "3rd-devs 是"
Output: "什么项目"
❌ WRONG examples (DON'T DO THIS):
Input: "医疗"
Output: "领域的FHIR标准评估确实是promptfoo最专业的..." ← NO! This is answering!
Input: "3rd-devs 是"
Output: "一个典型的企业级AI代理开发平台,它通过..." ← NO! This is explaining!
Input: "有哪些工具"
Output: "有很多工具可以使用,比如..." ← NO! This is responding!
Rule: If it sounds like you're answering or explaining, STOP. Just predict typing.
${hasContext ? '\nUse context for better prediction, but keep it SHORT.' : ''}`,
role: 'system',
},
{
content: 'Predict what user types: "Can you help me"',
role: 'user',
},
{
content: 'with this problem',
role: 'assistant',
},
{
content: 'Predict what user types: "我想要"',
role: 'user',
},
{
content: '学习 Python',
role: 'assistant',
},
...(hasContext
? [
{
content: `Context:\n${context.join('\n')}`,
role: 'user' as const,
},
]
: []),
{
content: `Predict what user types: "${input}"`,
role: 'user',
},
],
};
};
@@ -1,14 +1,14 @@
'use client';
import { Alert } from '@lobehub/ui';
import { Suspense, memo } from 'react';
import { Suspense, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { type ActionKeys, ChatInputProvider, DesktopChatInput } from '@/features/ChatInput';
import WideScreenContainer from '@/features/ChatList/components/WideScreenContainer';
import { useChatStore } from '@/store/chat';
import { aiChatSelectors } from '@/store/chat/selectors';
import WideScreenContainer from '@/features/Conversation/components/WideScreenContainer';
import { getChatStoreState, useChatStore } from '@/store/chat';
import { aiChatSelectors, displayMessageSelectors } from '@/store/chat/selectors';
import { useSend } from '../useSend';
import MessageFromUrl from './MessageFromUrl';
@@ -39,12 +39,17 @@ const ClassicChatInput = memo(() => {
const sendMenuItems = useSendMenuItems();
const getMessages = useCallback(() => {
return displayMessageSelectors.mainAIChats(getChatStoreState());
}, []);
return (
<ChatInputProvider
chatInputEditorRef={(instance) => {
if (!instance) return;
useChatStore.setState({ mainInputEditor: instance });
}}
getMessages={getMessages}
leftActions={leftActions}
onMarkdownContentChange={(content) => {
useChatStore.setState({ inputMessage: content });
@@ -20,6 +20,7 @@ export const ChatInputProvider = memo<ChatInputProviderProps>(
chatInputEditorRef,
onMarkdownContentChange,
mentionItems,
getMessages,
}) => {
const editor = useEditor();
const slashMenuRef = useRef<HTMLDivElement>(null);
@@ -41,6 +42,7 @@ export const ChatInputProvider = memo<ChatInputProviderProps>(
>
<StoreUpdater
chatInputEditorRef={chatInputEditorRef}
getMessages={getMessages}
leftActions={leftActions}
mentionItems={mentionItems}
mobile={mobile}
+22 -9
View File
@@ -4,6 +4,7 @@ import { isCommandPressed } from '@lobechat/utils';
import {
INSERT_MENTION_COMMAND,
INSERT_TABLE_COMMAND,
ReactAutoCompletePlugin,
ReactCodePlugin,
ReactCodeblockPlugin,
ReactHRPlugin,
@@ -33,15 +34,23 @@ const className = cx(css`
`);
const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
const [editor, slashMenuRef, send, updateMarkdownContent, expand, mentionItems] =
useChatInputStore((s) => [
s.editor,
s.slashMenuRef,
s.handleSendButton,
s.updateMarkdownContent,
s.expand,
s.mentionItems,
]);
const [
editor,
slashMenuRef,
send,
updateMarkdownContent,
expand,
mentionItems,
autoCompleteInput,
] = useChatInputStore((s) => [
s.editor,
s.slashMenuRef,
s.handleSendButton,
s.updateMarkdownContent,
s.expand,
s.mentionItems,
s.autoCompleteInput,
]);
const storeApi = useStoreApi();
const state = useEditorState(editor);
@@ -105,6 +114,10 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
/>
),
}),
Editor.withProps(ReactAutoCompletePlugin, {
delay: 500,
onAutoComplete: autoCompleteInput,
}),
],
},
[enableRichRender],
+2
View File
@@ -21,12 +21,14 @@ const StoreUpdater = memo<StoreUpdaterProps>(
onMarkdownContentChange,
sendMenu,
mentionItems,
getMessages,
}) => {
const storeApi = useStoreApi();
const useStoreUpdater = createStoreUpdater(storeApi);
const editor = useChatInputEditor();
useStoreUpdater('mobile', mobile!);
useStoreUpdater('getMessages', getMessages!);
useStoreUpdater('sendMenu', sendMenu!);
useStoreUpdater('mentionItems', mentionItems);
useStoreUpdater('leftActions', leftActions!);
+60 -1
View File
@@ -1,8 +1,17 @@
import { ContextEngine, GroupMessageFlattenProcessor } from '@lobechat/context-engine';
import { contextInputAutoComplete } from '@lobechat/prompts';
import { StateCreator } from 'zustand/vanilla';
import { chatService } from '@/services/chat';
import { PublicState, State, initialState } from './initialState';
export interface Action {
autoCompleteInput: (opts: {
afterText: string;
input: string;
selectionType: string;
}) => Promise<string | null>;
getJSONState: () => any;
getMarkdownContent: () => string;
handleSendButton: () => void;
@@ -24,6 +33,56 @@ type CreateStore = (
export const store: CreateStore = (publicState) => (set, get) => ({
...initialState,
...publicState,
autoCompleteInput: async ({ input }) => {
if (!input) return null;
// Clear previous timer
const timerId = get().autoCompleteTimerId;
if (timerId) {
clearTimeout(timerId);
}
// Increment request ID to track the latest request
const currentRequestId = get().autoCompleteRequestId + 1;
set({ autoCompleteRequestId: currentRequestId });
// Create a debounced promise
return new Promise<string | null>((resolve) => {
const newTimerId = setTimeout(async () => {
// Check if this is still the latest request
if (get().autoCompleteRequestId !== currentRequestId) {
resolve(null);
return;
}
let contexts: string[] = [];
if (get().getMessages) {
const { messages } = await new ContextEngine({
pipeline: [new GroupMessageFlattenProcessor()],
}).process({ messages: get().getMessages!() });
contexts = [messages.at(-1)?.content];
}
let result = '';
await chatService.fetchPresetTaskResult({
onFinish: async (text) => {
result = text;
},
params: {
...contextInputAutoComplete({ context: contexts, input }),
model: 'openai/gpt-oss-120b',
provider: 'groq',
},
});
resolve(result || null);
}, 300);
set({ autoCompleteTimerId: newTimerId });
});
},
getJSONState: () => {
return get().editor?.getDocument('json');
@@ -31,6 +90,7 @@ export const store: CreateStore = (publicState) => (set, get) => ({
getMarkdownContent: () => {
return String(get().editor?.getDocument('markdown') || '').trimEnd();
},
handleSendButton: () => {
if (!get().editor) return;
@@ -60,7 +120,6 @@ export const store: CreateStore = (publicState) => (set, get) => ({
setShowTypoBar: (showTypoBar) => {
set({ showTypoBar });
},
updateMarkdownContent: () => {
if (!get().onMarkdownContentChange) return;
@@ -3,6 +3,7 @@ import type { ChatInputProps } from '@lobehub/editor/react';
import type { MenuProps } from '@lobehub/ui/es/Menu';
import { ActionKeys } from '@/features/ChatInput';
import { UIChatMessage } from '@/types/message';
export type SendButtonHandler = (params: {
clearContent: () => void;
@@ -26,6 +27,10 @@ export const initialSendButtonState: SendButtonProps = {
export interface PublicState {
allowExpand?: boolean;
expand?: boolean;
/**
* 用于自动感知上下文
*/
getMessages?: () => UIChatMessage[];
leftActions: ActionKeys[];
mentionItems?: SlashOptions['items'];
mobile?: boolean;
@@ -38,6 +43,9 @@ export interface PublicState {
}
export interface State extends PublicState {
autoCompleteRequestId: number;
// eslint-disable-next-line no-undef
autoCompleteTimerId?: NodeJS.Timeout;
editor?: IEditor;
isContentEmpty: boolean;
markdownContent: string;
@@ -46,6 +54,7 @@ export interface State extends PublicState {
export const initialState: State = {
allowExpand: true,
autoCompleteRequestId: 0,
expand: false,
isContentEmpty: false,
leftActions: [],