diff --git a/wren-ui/public/images/learning/question-sql-pairs.png b/wren-ui/public/images/learning/question-sql-pairs.png
new file mode 100644
index 0000000000..5420b7a49b
Binary files /dev/null and b/wren-ui/public/images/learning/question-sql-pairs.png differ
diff --git a/wren-ui/src/components/learning/guide/stories.tsx b/wren-ui/src/components/learning/guide/stories.tsx
index 010c774002..4f49ac66a1 100644
--- a/wren-ui/src/components/learning/guide/stories.tsx
+++ b/wren-ui/src/components/learning/guide/stories.tsx
@@ -2,7 +2,9 @@ import { useState } from 'react';
import { createRoot } from 'react-dom/client';
import { NextRouter } from 'next/router';
import { Select } from 'antd';
+import styled from 'styled-components';
import { ModelIcon, TranslateIcon } from '@/utils/icons';
+import { RobotSVG } from '@/utils/svgs';
import { renderToString } from 'react-dom/server';
import {
Dispatcher,
@@ -19,6 +21,13 @@ import {
import { TEMPLATE_OPTIONS as SAMPLE_DATASET_INFO } from '@/components/pages/setup/utils';
import { getLanguageText } from '@/utils/language';
import * as events from '@/utils/events';
+import { nextTick } from '@/utils/time';
+
+const RobotIcon = styled(RobotSVG)`
+ width: 24px;
+ height: 24px;
+`;
+
const defaultConfigs: DriverConfig = {
progressText: '{{current}} / {{total}}',
nextBtnText: 'Next',
@@ -41,6 +50,10 @@ export const makeStoriesPlayer =
playDataModelingGuide(...args, dispatcher),
[LEARNING.SWITCH_PROJECT_LANGUAGE]: () =>
playSwitchProjectLanguageGuide(...args, dispatcher),
+ [LEARNING.QUESTION_SQL_PAIRS_GUIDE]: () =>
+ playQuestionSQLPairsGuide(...args, dispatcher),
+ [LEARNING.SAVE_TO_KNOWLEDGE]: () =>
+ playSaveToKnowledgeGuide(...args, dispatcher),
}[id] || null;
return action && action();
};
@@ -350,3 +363,173 @@ const playSwitchProjectLanguageGuide = (
]);
$driver.drive();
};
+
+const playQuestionSQLPairsGuide = (
+ $driver: DriverObj,
+ _router: NextRouter,
+ _payload: StoryPayload,
+ dispatcher: Dispatcher,
+) => {
+ if ($driver === null) {
+ console.error('Driver object is not initialized.');
+ return;
+ }
+
+ if ($driver.isActive()) $driver.destroy();
+
+ $driver.setConfig({ ...defaultConfigs, showProgress: true });
+ $driver.setSteps([
+ {
+ popover: {
+ title: renderToString(
+
+
+

+
+ Build Your Knowledge Base
+
,
+ ),
+ description: renderToString(
+ <>
+ Create and manage Question-SQL Pairs to refine Wren AI’s SQL
+ generation. You can manually add pairs here or go to Home, ask a
+ question, and save the correct answer to Knowledge. The more you
+ save, the smarter Wren AI becomes!
+ >,
+ ),
+ onPopoverRender: (popoverDom: DriverPopoverDOM) => {
+ resetPopoverStyle(popoverDom, 640);
+ },
+ doneBtnText: 'Get Started',
+ onNextClick: () => {
+ $driver.destroy();
+ dispatcher?.onDone && dispatcher.onDone();
+ },
+ },
+ },
+ ]);
+ $driver.drive();
+};
+
+const playSaveToKnowledgeGuide = async (
+ $driver: DriverObj,
+ _router: NextRouter,
+ _payload: StoryPayload,
+ dispatcher: Dispatcher,
+) => {
+ if ($driver === null) {
+ console.error('Driver object is not initialized.');
+ return;
+ }
+ if ($driver.isActive()) $driver.destroy();
+
+ $driver.setConfig({ ...defaultConfigs, showProgress: false });
+
+ const selectors = {
+ saveToKnowledge:
+ '[data-guideid="last-answer-result"] [data-guideid="save-to-knowledge"]',
+ previewData:
+ '[data-guideid="last-answer-result"] [data-guideid="text-answer-preview-data"]',
+ };
+
+ $driver.setSteps([
+ {
+ element: selectors.saveToKnowledge,
+ popover: {
+ side: 'top',
+ align: 'start',
+ title: renderToString(
+ <>
+
+
+
+ Save to Knowledge
+ >,
+ ),
+ description: renderToString(
+ <>
+ If the AI-generated answer is correct, save it as a{' '}
+ Question-SQL Pairs to improve AI learning. If it's incorrect,
+ refine it with follow-ups before saving to ensure accuracy.
+ >,
+ ),
+ onPopoverRender: (popoverDom: DriverPopoverDOM) => {
+ resetPopoverStyle(popoverDom, 360);
+ },
+ doneBtnText: 'Got it',
+ onNextClick: () => {
+ $driver.destroy();
+ dispatcher?.onDone && dispatcher.onDone();
+ },
+ },
+ },
+ ]);
+
+ let mutationObserver: MutationObserver | null = null;
+ let intersectionObserver: IntersectionObserver | null = null;
+
+ const cleanMutationObserverup = () => {
+ // if MutationObserver is listening to the element, disable it
+ if (mutationObserver) {
+ mutationObserver.disconnect();
+ mutationObserver = null;
+ }
+ };
+
+ const cleanIntersectionObserverup = () => {
+ if (intersectionObserver) {
+ intersectionObserver.disconnect();
+ intersectionObserver = null;
+ }
+ };
+
+ const startDriver = () => {
+ const target = document.querySelector(
+ selectors.previewData,
+ ) as HTMLElement | null;
+
+ if (!target) return false;
+
+ cleanMutationObserverup();
+
+ // use IntersectionObserver to ensure the element is in viewport before driving
+ intersectionObserver = new IntersectionObserver(
+ async (entries) => {
+ for (const entry of entries) {
+ if (entry.isIntersecting) {
+ cleanIntersectionObserverup();
+
+ await nextTick(700);
+ $driver.drive();
+ return;
+ }
+ }
+ },
+ { threshold: 0.5 }, // 50% of the element is visible
+ );
+
+ intersectionObserver.observe(target);
+ return true;
+ };
+
+ // try to start Driver.js
+ if (startDriver()) return;
+
+ // if the target element not appear, use MutationObserver to listen DOM changes
+ mutationObserver = new MutationObserver(() => {
+ if (startDriver()) {
+ cleanMutationObserverup();
+ }
+ });
+
+ mutationObserver.observe(document.body, { childList: true, subtree: true });
+
+ // 60 seconds after, observer will be cleared
+ await nextTick(60000);
+ cleanMutationObserverup();
+ cleanIntersectionObserverup();
+};
diff --git a/wren-ui/src/components/learning/guide/utils.ts b/wren-ui/src/components/learning/guide/utils.ts
index e23fd3f816..4f68bec47d 100644
--- a/wren-ui/src/components/learning/guide/utils.ts
+++ b/wren-ui/src/components/learning/guide/utils.ts
@@ -22,4 +22,8 @@ export enum LEARNING {
SWITCH_PROJECT_LANGUAGE = 'SWITCH_PROJECT_LANGUAGE',
SHARE_RESULTS = 'SHARE_RESULTS',
VIEW_FULL_SQL = 'VIEW_FULL_SQL',
+
+ // knowledge
+ QUESTION_SQL_PAIRS_GUIDE = 'QUESTION_SQL_PAIRS_GUIDE',
+ SAVE_TO_KNOWLEDGE = 'SAVE_TO_KNOWLEDGE',
}
diff --git a/wren-ui/src/components/learning/index.tsx b/wren-ui/src/components/learning/index.tsx
index 74dbb1e30b..5441bf7010 100644
--- a/wren-ui/src/components/learning/index.tsx
+++ b/wren-ui/src/components/learning/index.tsx
@@ -262,39 +262,63 @@ export default function SidebarSection(_props: Props) {
stories.some((item) => !learningRecord.paths.includes(item.id)),
);
- // play the data modeling guide if it's not finished
- if (
- router.pathname === Path.Modeling &&
- !learningRecord.paths.includes(LEARNING.DATA_MODELING_GUIDE)
- ) {
- nextTick(1000).then(() => {
+ const routerAction = {
+ [Path.Modeling]: async () => {
+ const isGuideDone = learningRecord.paths.includes(
+ LEARNING.DATA_MODELING_GUIDE,
+ );
const isSkipBefore = !!window.sessionStorage.getItem(
'skipDataModelingGuide',
);
- if (isSkipBefore) return;
- $guide.current?.play(LEARNING.DATA_MODELING_GUIDE, {
- onDone: () => saveRecord(LEARNING.DATA_MODELING_GUIDE),
- });
- });
- }
-
- if (
- router.pathname === Path.Home &&
- !learningRecord.paths.includes(LEARNING.SWITCH_PROJECT_LANGUAGE)
- ) {
- nextTick(1000).then(() => {
+ if (!(isGuideDone || isSkipBefore)) {
+ await nextTick(1000);
+ $guide.current?.play(LEARNING.DATA_MODELING_GUIDE, {
+ onDone: () => saveRecord(LEARNING.DATA_MODELING_GUIDE),
+ });
+ }
+ },
+ [Path.Home]: async () => {
+ const isGuideDone = learningRecord.paths.includes(
+ LEARNING.SWITCH_PROJECT_LANGUAGE,
+ );
const isSkipBefore = !!window.sessionStorage.getItem(
'skipSwitchProjectLanguageGuide',
);
- if (isSkipBefore) return;
- $guide.current?.play(LEARNING.SWITCH_PROJECT_LANGUAGE, {
- onDone: () => saveRecord(LEARNING.SWITCH_PROJECT_LANGUAGE),
- onSaveLanguage: saveLanguage,
- });
- });
- }
+ if (!(isGuideDone || isSkipBefore)) {
+ await nextTick(1000);
+ $guide.current?.play(LEARNING.SWITCH_PROJECT_LANGUAGE, {
+ onDone: () => saveRecord(LEARNING.SWITCH_PROJECT_LANGUAGE),
+ onSaveLanguage: saveLanguage,
+ });
+ }
+ },
+ [Path.Thread]: async () => {
+ const isGuideDone = learningRecord.paths.includes(
+ LEARNING.SAVE_TO_KNOWLEDGE,
+ );
+ if (!isGuideDone) {
+ await nextTick(1500);
+ $guide.current?.play(LEARNING.SAVE_TO_KNOWLEDGE, {
+ onDone: () => saveRecord(LEARNING.SAVE_TO_KNOWLEDGE),
+ });
+ }
+ },
+ [Path.KnowledgeQuestionSQLPairs]: async () => {
+ const isGuideDone = learningRecord.paths.includes(
+ LEARNING.QUESTION_SQL_PAIRS_GUIDE,
+ );
+ if (!isGuideDone) {
+ await nextTick(1000);
+ $guide.current?.play(LEARNING.QUESTION_SQL_PAIRS_GUIDE, {
+ onDone: () => saveRecord(LEARNING.QUESTION_SQL_PAIRS_GUIDE),
+ });
+ }
+ },
+ };
+
+ routerAction[router.pathname] && routerAction[router.pathname]();
}
- }, [learningRecordResult?.learningRecord]);
+ }, [learningRecordResult?.learningRecord, router.pathname]);
useEffect(() => {
collapseBlock(active);
@@ -305,35 +329,35 @@ export default function SidebarSection(_props: Props) {
};
// Hide learning section if the page not in whitelist
- if (!isLearningAccessible(router.pathname)) return null;
-
return (
<>
-
-
-
-
- Learning
+ {isLearningAccessible(router.pathname) && (
+
+
-
+
+
+
+
+
+ {current}/{total} Finished
+
+
+
-
-
-
-
-
- {current}/{total} Finished
-
-
-
-
+ )}
>
);
}
diff --git a/wren-ui/src/components/pages/home/promptThread/AnswerResult.tsx b/wren-ui/src/components/pages/home/promptThread/AnswerResult.tsx
index 02e830334e..96f4194e16 100644
--- a/wren-ui/src/components/pages/home/promptThread/AnswerResult.tsx
+++ b/wren-ui/src/components/pages/home/promptThread/AnswerResult.tsx
@@ -273,6 +273,7 @@ export default function AnswerResult(props: Props) {
{ isCreateMode: true },
)
}
+ data-guideid="save-to-knowledge"
>
diff --git a/wren-ui/src/components/pages/home/promptThread/TextBasedAnswer.tsx b/wren-ui/src/components/pages/home/promptThread/TextBasedAnswer.tsx
index 1fb6dcbf9c..b7468ca135 100644
--- a/wren-ui/src/components/pages/home/promptThread/TextBasedAnswer.tsx
+++ b/wren-ui/src/components/pages/home/promptThread/TextBasedAnswer.tsx
@@ -177,7 +177,10 @@ export default function TextBasedAnswer(
{previewDataResult?.data?.previewData && (
-
+
Considering the limit of the context window, we retrieve up to
500 rows of results to generate the answer.
diff --git a/wren-ui/src/components/pages/home/promptThread/index.tsx b/wren-ui/src/components/pages/home/promptThread/index.tsx
index aded37ac5b..4976b68fd1 100644
--- a/wren-ui/src/components/pages/home/promptThread/index.tsx
+++ b/wren-ui/src/components/pages/home/promptThread/index.tsx
@@ -98,7 +98,10 @@ const AnswerResultTemplate: React.FC<
const isLastThreadResponse = id === lastResponseId;
return (
-