update research mode, add search web and fetch url content
This commit is contained in:
parent
05b1302a6c
commit
9a5e5f3880
@ -62,6 +62,7 @@
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"@tanstack/react-query": "^5.56.2",
|
||||
"axios": "^1.8.3",
|
||||
"clsx": "^2.1.1",
|
||||
"diff": "^7.0.0",
|
||||
"drizzle-orm": "^0.35.2",
|
||||
|
||||
134
pnpm-lock.yaml
generated
134
pnpm-lock.yaml
generated
@ -44,6 +44,9 @@ importers:
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.56.2
|
||||
version: 5.66.0(react@18.3.1)
|
||||
axios:
|
||||
specifier: ^1.8.3
|
||||
version: 1.8.3
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
@ -76,7 +79,7 @@ importers:
|
||||
version: 2.2.3
|
||||
langchain:
|
||||
specifier: ^0.3.2
|
||||
version: 0.3.15(@langchain/core@0.3.40(openai@4.85.1(ws@8.18.0)(zod@3.24.2)))(handlebars@4.7.8)(openai@4.85.1(ws@8.18.0)(zod@3.24.2))(ws@8.18.0)
|
||||
version: 0.3.15(@langchain/core@0.3.40(openai@4.85.1(ws@8.18.0)(zod@3.24.2)))(axios@1.8.3)(handlebars@4.7.8)(openai@4.85.1(ws@8.18.0)(zod@3.24.2))(ws@8.18.0)
|
||||
lexical:
|
||||
specifier: ^0.17.1
|
||||
version: 0.17.1
|
||||
@ -128,6 +131,9 @@ importers:
|
||||
react-syntax-highlighter:
|
||||
specifier: ^15.5.0
|
||||
version: 15.6.1(react@18.3.1)
|
||||
rehype-raw:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
remark-gfm:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.1
|
||||
@ -1788,6 +1794,9 @@ packages:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
axios@1.8.3:
|
||||
resolution: {integrity: sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==}
|
||||
|
||||
babel-jest@29.7.0:
|
||||
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@ -2526,6 +2535,15 @@ packages:
|
||||
flatted@3.3.2:
|
||||
resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
|
||||
|
||||
follow-redirects@1.15.9:
|
||||
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
for-each@0.3.5:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -2694,18 +2712,33 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hast-util-from-parse5@8.0.3:
|
||||
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
|
||||
|
||||
hast-util-parse-selector@2.2.5:
|
||||
resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==}
|
||||
|
||||
hast-util-parse-selector@4.0.0:
|
||||
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
|
||||
|
||||
hast-util-raw@9.1.0:
|
||||
resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==}
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.2:
|
||||
resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==}
|
||||
|
||||
hast-util-to-parse5@8.0.0:
|
||||
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
||||
|
||||
hastscript@6.0.0:
|
||||
resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==}
|
||||
|
||||
hastscript@9.0.1:
|
||||
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
|
||||
|
||||
highlight.js@10.7.3:
|
||||
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||
|
||||
@ -2729,6 +2762,9 @@ packages:
|
||||
html-url-attributes@3.0.1:
|
||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
http-proxy-agent@5.0.0:
|
||||
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
|
||||
engines: {node: '>= 6'}
|
||||
@ -3811,6 +3847,12 @@ packages:
|
||||
property-information@6.5.0:
|
||||
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
|
||||
|
||||
property-information@7.0.0:
|
||||
resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
psl@1.15.0:
|
||||
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
|
||||
|
||||
@ -3903,6 +3945,9 @@ packages:
|
||||
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
rehype-raw@7.0.0:
|
||||
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
||||
|
||||
@ -4365,6 +4410,9 @@ packages:
|
||||
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
|
||||
vfile-location@5.0.3:
|
||||
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
|
||||
|
||||
vfile-message@4.0.2:
|
||||
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
|
||||
|
||||
@ -4381,6 +4429,9 @@ packages:
|
||||
walker@1.0.8:
|
||||
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
|
||||
|
||||
web-namespaces@2.0.1:
|
||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3:
|
||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||
engines: {node: '>= 14'}
|
||||
@ -6174,6 +6225,14 @@ snapshots:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.1.0
|
||||
|
||||
axios@1.8.3:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.9
|
||||
form-data: 4.0.2
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
babel-jest@29.7.0(@babel/core@7.26.9):
|
||||
dependencies:
|
||||
'@babel/core': 7.26.9
|
||||
@ -7020,6 +7079,8 @@ snapshots:
|
||||
|
||||
flatted@3.3.2: {}
|
||||
|
||||
follow-redirects@1.15.9: {}
|
||||
|
||||
for-each@0.3.5:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
@ -7202,8 +7263,39 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hast-util-from-parse5@8.0.3:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
devlop: 1.1.0
|
||||
hastscript: 9.0.1
|
||||
property-information: 7.0.0
|
||||
vfile: 6.0.3
|
||||
vfile-location: 5.0.3
|
||||
web-namespaces: 2.0.1
|
||||
|
||||
hast-util-parse-selector@2.2.5: {}
|
||||
|
||||
hast-util-parse-selector@4.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hast-util-raw@9.1.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
'@ungap/structured-clone': 1.3.0
|
||||
hast-util-from-parse5: 8.0.3
|
||||
hast-util-to-parse5: 8.0.0
|
||||
html-void-elements: 3.0.0
|
||||
mdast-util-to-hast: 13.2.0
|
||||
parse5: 7.2.1
|
||||
unist-util-position: 5.0.0
|
||||
unist-util-visit: 5.0.0
|
||||
vfile: 6.0.3
|
||||
web-namespaces: 2.0.1
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.2:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.6
|
||||
@ -7224,6 +7316,16 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
hast-util-to-parse5@8.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
comma-separated-tokens: 2.0.3
|
||||
devlop: 1.1.0
|
||||
property-information: 6.5.0
|
||||
space-separated-tokens: 2.0.2
|
||||
web-namespaces: 2.0.1
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
@ -7236,6 +7338,14 @@ snapshots:
|
||||
property-information: 5.6.0
|
||||
space-separated-tokens: 1.1.5
|
||||
|
||||
hastscript@9.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
comma-separated-tokens: 2.0.3
|
||||
hast-util-parse-selector: 4.0.0
|
||||
property-information: 7.0.0
|
||||
space-separated-tokens: 2.0.2
|
||||
|
||||
highlight.js@10.7.3: {}
|
||||
|
||||
highlightjs-vue@1.0.0: {}
|
||||
@ -7252,6 +7362,8 @@ snapshots:
|
||||
|
||||
html-url-attributes@3.0.1: {}
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
http-proxy-agent@5.0.0:
|
||||
dependencies:
|
||||
'@tootallnate/once': 2.0.0
|
||||
@ -7934,7 +8046,7 @@ snapshots:
|
||||
|
||||
known-css-properties@0.35.0: {}
|
||||
|
||||
langchain@0.3.15(@langchain/core@0.3.40(openai@4.85.1(ws@8.18.0)(zod@3.24.2)))(handlebars@4.7.8)(openai@4.85.1(ws@8.18.0)(zod@3.24.2))(ws@8.18.0):
|
||||
langchain@0.3.15(@langchain/core@0.3.40(openai@4.85.1(ws@8.18.0)(zod@3.24.2)))(axios@1.8.3)(handlebars@4.7.8)(openai@4.85.1(ws@8.18.0)(zod@3.24.2))(ws@8.18.0):
|
||||
dependencies:
|
||||
'@langchain/core': 0.3.40(openai@4.85.1(ws@8.18.0)(zod@3.24.2))
|
||||
'@langchain/openai': 0.4.4(@langchain/core@0.3.40(openai@4.85.1(ws@8.18.0)(zod@3.24.2)))(ws@8.18.0)
|
||||
@ -7950,6 +8062,7 @@ snapshots:
|
||||
zod: 3.24.2
|
||||
zod-to-json-schema: 3.24.1(zod@3.24.2)
|
||||
optionalDependencies:
|
||||
axios: 1.8.3
|
||||
handlebars: 4.7.8
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
@ -8696,6 +8809,10 @@ snapshots:
|
||||
|
||||
property-information@6.5.0: {}
|
||||
|
||||
property-information@7.0.0: {}
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
psl@1.15.0:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
@ -8809,6 +8926,12 @@ snapshots:
|
||||
gopd: 1.2.0
|
||||
set-function-name: 2.0.2
|
||||
|
||||
rehype-raw@7.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-raw: 9.1.0
|
||||
vfile: 6.0.3
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
@ -9370,6 +9493,11 @@ snapshots:
|
||||
'@types/istanbul-lib-coverage': 2.0.6
|
||||
convert-source-map: 2.0.0
|
||||
|
||||
vfile-location@5.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
vfile: 6.0.3
|
||||
|
||||
vfile-message@4.0.2:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@ -9390,6 +9518,8 @@ snapshots:
|
||||
dependencies:
|
||||
makeerror: 1.0.12
|
||||
|
||||
web-namespaces@2.0.1: {}
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
@ -44,6 +44,7 @@ import {
|
||||
import { readTFileContent } from '../../utils/obsidian'
|
||||
import { openSettingsModalWithError } from '../../utils/open-settings-modal'
|
||||
import { PromptGenerator, addLineNumbers } from '../../utils/prompt-generator'
|
||||
import { fetchUrlsContent, webSearch } from '../../utils/web-search'
|
||||
|
||||
// Simple file reading function that returns a placeholder content for testing
|
||||
const readFileContent = (filePath: string): string => {
|
||||
@ -283,7 +284,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
chatModel,
|
||||
{
|
||||
model: chatModel.modelId,
|
||||
temperature: 0.5,
|
||||
temperature: 0,
|
||||
messages: requestMessages,
|
||||
stream: true,
|
||||
},
|
||||
@ -532,6 +533,38 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
mentionables: [],
|
||||
}
|
||||
}
|
||||
} else if (toolArgs.type === 'search_web') {
|
||||
const results = await webSearch(toolArgs.query)
|
||||
const formattedContent = `[search_web for '${toolArgs.query}'] Result:\n${results}\n`;
|
||||
return {
|
||||
type: 'search_web',
|
||||
applyMsgId,
|
||||
applyStatus: ApplyStatus.Applied,
|
||||
returnMsg: {
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: null,
|
||||
promptContent: formattedContent,
|
||||
id: uuidv4(),
|
||||
mentionables: [],
|
||||
}
|
||||
}
|
||||
} else if (toolArgs.type === 'fetch_urls_content') {
|
||||
const results = await fetchUrlsContent(toolArgs.urls)
|
||||
const formattedContent = `[ fetch_urls_content ] Result:\n${results}\n`;
|
||||
return {
|
||||
type: 'fetch_urls_content',
|
||||
applyMsgId,
|
||||
applyStatus: ApplyStatus.Applied,
|
||||
returnMsg: {
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: null,
|
||||
promptContent: formattedContent,
|
||||
id: uuidv4(),
|
||||
mentionables: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to apply changes', error)
|
||||
|
||||
81
src/components/chat-view/MarkdownFetchUrlsContentBlock.tsx
Normal file
81
src/components/chat-view/MarkdownFetchUrlsContentBlock.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { ChevronDown, ChevronRight, Globe } from 'lucide-react'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { useDarkModeContext } from '../../contexts/DarkModeContext'
|
||||
import { ApplyStatus, FetchUrlsContentToolArgs } from '../../types/apply'
|
||||
|
||||
import { MemoizedSyntaxHighlighterWrapper } from './SyntaxHighlighterWrapper'
|
||||
|
||||
export default function MarkdownFetchUrlsContentBlock({
|
||||
applyStatus,
|
||||
onApply,
|
||||
urls,
|
||||
finish
|
||||
}: {
|
||||
applyStatus: ApplyStatus
|
||||
onApply: (args: FetchUrlsContentToolArgs) => void
|
||||
urls: string[],
|
||||
finish: boolean
|
||||
}) {
|
||||
const { isDarkMode } = useDarkModeContext()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log('finish', finish, applyStatus)
|
||||
if (finish && applyStatus === ApplyStatus.Idle) {
|
||||
console.log('finish auto fetch urls content', urls)
|
||||
onApply({
|
||||
type: 'fetch_urls_content',
|
||||
urls: urls
|
||||
})
|
||||
}
|
||||
}, [finish])
|
||||
|
||||
const urlsMarkdownContent = useMemo(() => {
|
||||
return urls.map(url => {
|
||||
return `${url}`
|
||||
}).join('\n\n')
|
||||
}, [urls])
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.scrollTop = containerRef.current.scrollHeight
|
||||
}
|
||||
}, [urlsMarkdownContent])
|
||||
|
||||
return (
|
||||
urlsMarkdownContent && (
|
||||
<div
|
||||
className={`infio-chat-code-block has-filename infio-reasoning-block`}
|
||||
>
|
||||
<div className={'infio-chat-code-block-header'}>
|
||||
<div className={'infio-chat-code-block-header-filename'}>
|
||||
<Globe size={10} className="infio-chat-code-block-header-icon" />
|
||||
Fetch URLs Content
|
||||
</div>
|
||||
<button
|
||||
className="clickable-icon infio-chat-list-dropdown"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{isOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="infio-reasoning-content-wrapper"
|
||||
>
|
||||
<MemoizedSyntaxHighlighterWrapper
|
||||
isDarkMode={isDarkMode}
|
||||
language="markdown"
|
||||
hasFilename={true}
|
||||
wrapLines={true}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
{urlsMarkdownContent}
|
||||
</MemoizedSyntaxHighlighterWrapper>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
41
src/components/chat-view/MarkdownSearchWebBlock.tsx
Normal file
41
src/components/chat-view/MarkdownSearchWebBlock.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { Search } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
import { ApplyStatus, SearchWebToolArgs } from '../../types/apply'
|
||||
|
||||
export default function MarkdownWebSearchBlock({
|
||||
applyStatus,
|
||||
onApply,
|
||||
query,
|
||||
finish
|
||||
}: {
|
||||
applyStatus: ApplyStatus
|
||||
onApply: (args: SearchWebToolArgs) => void
|
||||
query: string,
|
||||
finish: boolean
|
||||
}) {
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log('finish', finish, applyStatus)
|
||||
if (finish && applyStatus === ApplyStatus.Idle) {
|
||||
console.log('finish auto web search', query)
|
||||
onApply({
|
||||
type: 'search_web',
|
||||
query: query,
|
||||
})
|
||||
}
|
||||
}, [finish])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`infio-chat-code-block has-filename`}
|
||||
>
|
||||
<div className={'infio-chat-code-block-header'}>
|
||||
<div className={'infio-chat-code-block-header-filename'}>
|
||||
<Search size={14} className="infio-chat-code-block-header-icon" />
|
||||
Web search: {query}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -8,14 +8,15 @@ import {
|
||||
} from '../../utils/parse-infio-block'
|
||||
|
||||
import MarkdownEditFileBlock from './MarkdownEditFileBlock'
|
||||
import MarkdownFetchUrlsContentBlock from './MarkdownFetchUrlsContentBlock'
|
||||
import MarkdownListFilesBlock from './MarkdownListFilesBlock'
|
||||
import MarkdownReadFileBlock from './MarkdownReadFileBlock'
|
||||
import MarkdownReasoningBlock from './MarkdownReasoningBlock'
|
||||
import MarkdownRegexSearchFilesBlock from './MarkdownRegexSearchFilesBlock'
|
||||
import MarkdownSearchAndReplace from './MarkdownSearchAndReplace'
|
||||
import MarkdownSearchWebBlock from './MarkdownSearchWebBlock'
|
||||
import MarkdownSemanticSearchFilesBlock from './MarkdownSemanticSearchFilesBlock'
|
||||
import MarkdownWithIcons from './MarkdownWithIcon'
|
||||
|
||||
function ReactMarkdown({
|
||||
applyStatus,
|
||||
onApply,
|
||||
@ -131,6 +132,22 @@ function ReactMarkdown({
|
||||
markdownContent={
|
||||
`<icon name='ask_followup_question' size={14} className="infio-markdown-icon" />
|
||||
${block.question && block.question.trimStart()}`} />
|
||||
) : block.type === 'search_web' ? (
|
||||
<MarkdownSearchWebBlock
|
||||
key={"search-web-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
query={block.query}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'fetch_urls_content' ? (
|
||||
<MarkdownFetchUrlsContentBlock
|
||||
key={"fetch-urls-content-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
urls={block.urls}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : (
|
||||
<Markdown key={"markdown-" + index} className="infio-markdown">
|
||||
{block.content}
|
||||
|
||||
@ -63,4 +63,17 @@ export type SearchAndReplaceToolArgs = {
|
||||
regexFlags?: string;
|
||||
}[];
|
||||
}
|
||||
export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs;
|
||||
|
||||
export type SearchWebToolArgs = {
|
||||
type: 'search_web';
|
||||
query: string;
|
||||
finish?: boolean;
|
||||
}
|
||||
|
||||
export type FetchUrlsContentToolArgs = {
|
||||
type: 'fetch_urls_content';
|
||||
urls: string[];
|
||||
finish?: boolean;
|
||||
}
|
||||
|
||||
export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs | SearchWebToolArgs | FetchUrlsContentToolArgs;
|
||||
|
||||
@ -78,6 +78,15 @@ export function getToolsForMode(groups: readonly GroupEntry[]): string[] {
|
||||
|
||||
// Main modes configuration as an ordered array
|
||||
export const modes: readonly ModeConfig[] = [
|
||||
{
|
||||
slug: "research",
|
||||
name: "Research",
|
||||
roleDefinition:
|
||||
"You are Infio, an advanced research assistant specialized in comprehensive investigation and analytical thinking. You excel at breaking down complex questions, exploring multiple perspectives, and synthesizing information to provide well-reasoned conclusions.",
|
||||
groups: ["research"],
|
||||
customInstructions:
|
||||
"You can conduct thorough research by analyzing available information, connecting related concepts, and applying structured reasoning methods. Help users explore topics in depth by considering multiple angles, identifying relevant evidence, and evaluating the reliability of sources. Use step-by-step analysis when tackling complex problems, explaining your thought process clearly. Create visual representations like Mermaid diagrams when they help clarify relationships between ideas. Use Markdown tables to present statistical data or comparative information when appropriate. Present balanced viewpoints while highlighting the strength of evidence behind different conclusions.",
|
||||
},
|
||||
{
|
||||
slug: "write",
|
||||
name: "Write",
|
||||
|
||||
@ -60,6 +60,14 @@ export type ParsedMsgBlock =
|
||||
path: string
|
||||
query: string
|
||||
finish: boolean
|
||||
} | {
|
||||
type: 'search_web'
|
||||
query: string
|
||||
finish: boolean
|
||||
} | {
|
||||
type: 'fetch_urls_content'
|
||||
urls: string[]
|
||||
finish: boolean
|
||||
}
|
||||
|
||||
export function parseMsgBlocks(
|
||||
@ -416,6 +424,66 @@ export function parseMsgBlocks(
|
||||
})
|
||||
lastEndOffset = endOffset
|
||||
}
|
||||
else if (node.nodeName === 'search_web') {
|
||||
if (!node.sourceCodeLocation) {
|
||||
throw new Error('sourceCodeLocation is undefined')
|
||||
}
|
||||
const startOffset = node.sourceCodeLocation.startOffset
|
||||
const endOffset = node.sourceCodeLocation.endOffset
|
||||
if (startOffset > lastEndOffset) {
|
||||
parsedResult.push({
|
||||
type: 'string',
|
||||
content: input.slice(lastEndOffset, startOffset),
|
||||
})
|
||||
}
|
||||
let query: string | undefined
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'query' && childNode.childNodes.length > 0) {
|
||||
query = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
parsedResult.push({
|
||||
type: 'search_web',
|
||||
query: query || '',
|
||||
finish: node.sourceCodeLocation.endTag !== undefined
|
||||
})
|
||||
lastEndOffset = endOffset
|
||||
} else if (node.nodeName === 'fetch_urls_content') {
|
||||
if (!node.sourceCodeLocation) {
|
||||
throw new Error('sourceCodeLocation is undefined')
|
||||
}
|
||||
const startOffset = node.sourceCodeLocation.startOffset
|
||||
const endOffset = node.sourceCodeLocation.endOffset
|
||||
if (startOffset > lastEndOffset) {
|
||||
parsedResult.push({
|
||||
type: 'string',
|
||||
content: input.slice(lastEndOffset, startOffset),
|
||||
})
|
||||
}
|
||||
|
||||
let urls: string[] = []
|
||||
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'urls' && childNode.childNodes.length > 0) {
|
||||
try {
|
||||
const urlsJson = childNode.childNodes[0].value
|
||||
const parsedUrls = JSON5.parse(urlsJson)
|
||||
if (Array.isArray(parsedUrls)) {
|
||||
urls = parsedUrls
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse URLs JSON', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsedResult.push({
|
||||
type: 'fetch_urls_content',
|
||||
urls,
|
||||
finish: node.sourceCodeLocation.endTag !== undefined
|
||||
})
|
||||
lastEndOffset = endOffset
|
||||
}
|
||||
}
|
||||
|
||||
// handle the last part of the input
|
||||
|
||||
@ -18,7 +18,6 @@ import {
|
||||
import { InfioSettings } from '../types/settings'
|
||||
import { defaultModeSlug, getFullModeDetails } from "../utils/modes"
|
||||
|
||||
import { listFilesAndFolders } from './glob-utils'
|
||||
import {
|
||||
readTFileContent
|
||||
} from './obsidian'
|
||||
|
||||
@ -30,6 +30,9 @@ export const TOOL_GROUPS: Record<string, ToolGroupConfig> = {
|
||||
edit: {
|
||||
tools: ["apply_diff", "write_to_file", "insert_content", "search_and_replace"],
|
||||
},
|
||||
research: {
|
||||
tools: ["search_web", "fetch_urls_content"],
|
||||
},
|
||||
// browser: {
|
||||
// tools: ["browser_action"],
|
||||
// },
|
||||
@ -52,7 +55,6 @@ export const ALWAYS_AVAILABLE_TOOLS = [
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
"switch_mode",
|
||||
"new_task",
|
||||
] as const
|
||||
|
||||
// Tool name types for type safety
|
||||
@ -71,6 +73,7 @@ export function getToolOptions(toolConfig: string | readonly [ToolName, ...any[]
|
||||
export const GROUP_DISPLAY_NAMES: Record<ToolGroup, string> = {
|
||||
read: "Read Files",
|
||||
edit: "Edit Files",
|
||||
research: "Research",
|
||||
browser: "Use Browser",
|
||||
command: "Run Commands",
|
||||
mcp: "Use MCP",
|
||||
|
||||
151
src/utils/web-search.ts
Normal file
151
src/utils/web-search.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import https from 'https';
|
||||
|
||||
import { htmlToMarkdown, requestUrl } from 'obsidian';
|
||||
|
||||
import { YoutubeTranscript, isYoutubeUrl } from './youtube-transcript';
|
||||
|
||||
const SERPER_API_KEY = 'a6fd4dc4b79f10b1e5008b688c81bacef0d24b4d5cd4e52071afa8329a67497c'
|
||||
|
||||
interface SearchResult {
|
||||
title: string;
|
||||
link: string;
|
||||
snippet: string;
|
||||
}
|
||||
|
||||
interface SearchResponse {
|
||||
organic_results?: SearchResult[];
|
||||
}
|
||||
|
||||
export async function webSearch(query: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `https://serpapi.com/search?q=${encodeURIComponent(query)}&engine=google&api_key=${SERPER_API_KEY}&num=20`;
|
||||
|
||||
console.log(url)
|
||||
|
||||
https.get(url, (res: any) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk: Buffer) => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
console.log(data)
|
||||
let parsedData: SearchResponse;
|
||||
try {
|
||||
parsedData = JSON.parse(data);
|
||||
} catch {
|
||||
parsedData = { organic_results: undefined };
|
||||
}
|
||||
const results = parsedData?.organic_results;
|
||||
|
||||
if (!results) {
|
||||
resolve('');
|
||||
return;
|
||||
}
|
||||
|
||||
const formattedResults = results.map((item: SearchResult) => {
|
||||
return `title: ${item.title}\nurl: ${item.link}\nsnippet: ${item.snippet}\n`;
|
||||
}).join('\n\n');
|
||||
|
||||
resolve(formattedResults);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}).on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function getWebsiteContent(url: string): Promise<string> {
|
||||
if (isYoutubeUrl(url)) {
|
||||
// TODO: pass language based on user preferences
|
||||
const { title, transcript } =
|
||||
await YoutubeTranscript.fetchTranscriptAndMetadata(url)
|
||||
|
||||
return `Title: ${title}
|
||||
Video Transcript:
|
||||
${transcript.map((t) => `${t.offset}: ${t.text}`).join('\n')}`
|
||||
}
|
||||
|
||||
const response = await requestUrl({ url })
|
||||
|
||||
return htmlToMarkdown(response.text)
|
||||
}
|
||||
|
||||
const USE_JINA = true
|
||||
|
||||
export async function fetchUrlsContent(urls: string[]): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const results = urls.map(async (url) => {
|
||||
try {
|
||||
const content = USE_JINA ? await fetchJina(url) : await getWebsiteContent(url);
|
||||
return `<url_content url="${url}">\n${content}\n</url_content>`;
|
||||
} catch (error) {
|
||||
console.error(`获取URL内容失败: ${url}`, error);
|
||||
return `<url_content url="${url}">\n获取内容失败: ${error}\n</url_content>`;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('fetchUrlsContent', results);
|
||||
|
||||
Promise.all(results).then((texts) => {
|
||||
resolve(texts.join('\n\n'));
|
||||
}).catch((error) => {
|
||||
console.error('获取URLs内容时出错', error);
|
||||
resolve('fetch urls content error'); // 即使出错也返回一些内容
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fetchJina(url: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const jinaUrl = `https://r.jina.ai/${url}`;
|
||||
|
||||
const jinaHeaders = {
|
||||
'Authorization': 'Bearer jina_1d721eb8c4814a938b4351ae0c3a0f117FlTTAz1GOmpOsIDN7HvIyLbiOCe',
|
||||
'X-No-Cache': 'true',
|
||||
};
|
||||
|
||||
const jinaOptions: https.RequestOptions = {
|
||||
method: 'GET',
|
||||
headers: jinaHeaders,
|
||||
};
|
||||
|
||||
const req = https.request(jinaUrl, jinaOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log(data);
|
||||
|
||||
try {
|
||||
// 检查是否有错误响应
|
||||
const response = JSON.parse(data);
|
||||
if (response.code && response.message) {
|
||||
console.error(`JINA API 错误: ${response.message}`);
|
||||
resolve(`无法获取内容: ${response.message}`);
|
||||
return;
|
||||
}
|
||||
resolve(data);
|
||||
} catch (e) {
|
||||
// 如果不是JSON格式,可能是正常的内容
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => {
|
||||
console.error(`Error: ${e.message}`);
|
||||
resolve(`fetch jina error: ${e.message}`);
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user