Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
183 changes: 183 additions & 0 deletions wren-ui/src/components/learning/guide/stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
Expand All @@ -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();
};
Expand Down Expand Up @@ -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(
<div className="pt-4">
<div className="-mx-4" style={{ minHeight: 317 }}>
<img
className="mb-4"
src="/images/learning/question-sql-pairs.png"
alt="question-sql-pairs-guide"
/>
</div>
Build Your Knowledge Base
</div>,
),
description: renderToString(
<>
Create and manage <b>Question-SQL Pairs</b> 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(
<>
<div className="mb-1">
<RobotIcon />
</div>
Save to Knowledge
</>,
),
description: renderToString(
<>
If the AI-generated answer is correct, save it as a{' '}
<b>Question-SQL Pairs</b> 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();
};
4 changes: 4 additions & 0 deletions wren-ui/src/components/learning/guide/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
124 changes: 74 additions & 50 deletions wren-ui/src/components/learning/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 (
<>
<LearningGuide ref={$guide} />
<div className="border-t border-gray-4">
<div
className="px-4 py-1 d-flex align-center cursor-pointer select-none"
onClick={onCollapseBarClick}
>
<div className="flex-grow-1">
<ReadOutlined className="mr-1" />
Learning
{isLearningAccessible(router.pathname) && (
<div className="border-t border-gray-4">
<div
className="px-4 py-1 d-flex align-center cursor-pointer select-none"
onClick={onCollapseBarClick}
>
<div className="flex-grow-1">
<ReadOutlined className="mr-1" />
Learning
</div>
<RightOutlined
className="text-sm"
style={{ transform: `rotate(${active ? '90deg' : '0deg'})` }}
/>
</div>
<RightOutlined
className="text-sm"
style={{ transform: `rotate(${active ? '90deg' : '0deg'})` }}
/>
<CollapseBlock ref={$collapseBlock}>
<ListIterator data={stories} />
<div className="px-4 py-2 d-flex align-center">
<Progress total={total} current={current} />
<span className="text-xs gray-6 text-nowrap pl-2">
{current}/{total} Finished
</span>
</div>
</CollapseBlock>
</div>
<CollapseBlock ref={$collapseBlock}>
<ListIterator data={stories} />
<div className="px-4 py-2 d-flex align-center">
<Progress total={total} current={current} />
<span className="text-xs gray-6 text-nowrap pl-2">
{current}/{total} Finished
</span>
</div>
</CollapseBlock>
</div>
)}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export default function AnswerResult(props: Props) {
{ isCreateMode: true },
)
}
data-guideid="save-to-knowledge"
>
<div className="d-flex align-center">
<RobotSVG className="mr-2" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,10 @@ export default function TextBasedAnswer(
</Button>

{previewDataResult?.data?.previewData && (
<div className="mt-2 mb-3">
<div
className="mt-2 mb-3"
data-guideid="text-answer-preview-data"
>
<Text type="secondary" className="text-sm">
Considering the limit of the context window, we retrieve up to
500 rows of results to generate the answer.
Expand Down
5 changes: 4 additions & 1 deletion wren-ui/src/components/pages/home/promptThread/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ const AnswerResultTemplate: React.FC<
const isLastThreadResponse = id === lastResponseId;

return (
<div key={`${id}-${index}`}>
<div
key={`${id}-${index}`}
data-guideid={isLastThreadResponse ? `last-answer-result` : undefined}
>
{index > 0 && <Divider />}
<AnswerResult
motion={motion}
Expand Down
Loading