diff --git a/entrypoints/background.js b/entrypoints/background.js
index 76e2ef6..df7e07c 100644
--- a/entrypoints/background.js
+++ b/entrypoints/background.js
@@ -1,6 +1,32 @@
import { browser } from "wxt/browser";
export default defineBackground(() => {
+ // browser.contextMenus.create({
+ // id: "explain",
+ // title: "问问AI",
+ // contexts: ["selection"]
+ // });
+
+ // browser.contextMenus.create({
+ // id: "summarize",
+ // title: "总结全文",
+ // contexts: ["page"]
+ // });
+
+ // browser.contextMenus.onClicked.addListener(async (info, tab) => {
+ // browser.scripting.executeScript({
+ // target: { tabId: tab.id },
+ // files: ['inject.js'],
+ // // args: [info]
+ // });
+ // if (info.menuItemId === "explain") {
+ // // 处理 AI 讲解逻辑
+
+ // } else if (info.menuItemId === "summarize") {
+ // // 处理 总结全文 逻辑
+ // console.log("总结全文:", tab.url);
+ // }
+ // });
browser.action.onClicked.addListener(function () {
browser.tabs.create({ url: browser.runtime.getURL("ext.html") });
});
diff --git a/entrypoints/content/App.jsx b/entrypoints/content/App.jsx
new file mode 100644
index 0000000..d7117cb
--- /dev/null
+++ b/entrypoints/content/App.jsx
@@ -0,0 +1,74 @@
+import { useState, useEffect } from 'react';
+import Modal from './components/Modal';
+import Logo from '@/public/logo.svg';
+
+const getPageContext = () => {
+ return {
+ content: document.body.innerText,
+ title: document.title,
+ keywords: document.querySelector('meta[name="keywords"]')?.content,
+ description: document.querySelector('meta[name="description"]')?.content,
+ };
+};
+
+export default ({ ctx }) => {
+ const [showPage, setShowPage] = useState(false);
+ const [showButton, setShowButton] = useState(false);
+ const [buttonPosition, setButtonPosition] = useState({ x: 0, y: 0 });
+ const [selection, setSelection] = useState('');
+ const [payload, setPayload] = useState({});
+
+ useEffect(() => {
+ const handleSelection = e => {
+ const selection = window.getSelection();
+ if (selection.toString().length > 0) {
+ setSelection(selection.toString());
+ setShowButton(true);
+ setButtonPosition({ x: e.clientX + 6, y: e.clientY + 8 });
+ } else {
+ setShowButton(false);
+ }
+ };
+
+ document.addEventListener('mouseup', handleSelection);
+ return () => document.removeEventListener('mouseup', handleSelection);
+ }, []);
+
+ useEffect(() => {
+ if (showPage) {
+ document.querySelector('html').style.overflow = 'hidden';
+ } else {
+ document.querySelector('html').style.overflow = 'auto';
+ }
+ }, [showPage]);
+
+ const handleButtonClick = () => {
+ setPayload({ selection, type: 'query', context: getPageContext() });
+ setShowPage(true);
+ setShowButton(false);
+ };
+
+ return (
+
+
setShowPage(false)}
+ payload={payload}
+ />
+
+
+
+
+ );
+};
diff --git a/entrypoints/content/components/Modal.jsx b/entrypoints/content/components/Modal.jsx
new file mode 100644
index 0000000..676e04a
--- /dev/null
+++ b/entrypoints/content/components/Modal.jsx
@@ -0,0 +1,42 @@
+import { useEffect } from 'react';
+import { useState } from 'react';
+
+export default function ({ close, payload, visible }) {
+ const url = browser.runtime.getURL('ext.html') + '#web-copilot';
+ const [loaded, setLoaded] = useState(false);
+ const iframeRef = useRef(null);
+
+ useEffect(() => {
+ if (!visible) {
+ iframeRef.current.contentWindow.postMessage(
+ JSON.stringify({ type: 'clear' }),
+ '*'
+ );
+ return;
+ }
+ if (payload.type) {
+ iframeRef.current.contentWindow.postMessage(JSON.stringify(payload), '*');
+ }
+ }, [payload, loaded, visible]);
+ return (
+
+
+
+ );
+}
diff --git a/entrypoints/content/index.jsx b/entrypoints/content/index.jsx
new file mode 100644
index 0000000..291a4b5
--- /dev/null
+++ b/entrypoints/content/index.jsx
@@ -0,0 +1,36 @@
+import './style.css';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+export default defineContentScript({
+ matches: ['*://*/*'],
+ cssInjectionMode: 'ui',
+
+ async main(ctx) {
+ console.log('content script injecting', ctx);
+ const ui = await createShadowRootUi(ctx, {
+ name: 'silo-ui',
+ position: 'overlay',
+ alignment: 'bottom-right',
+ zIndex: 99999,
+ anchor: 'body',
+ append: 'first',
+ onMount: container => {
+ // Don't mount react app directly on
+ const wrapper = document.createElement('div');
+ wrapper.className = '';
+ container.append(wrapper);
+
+ const root = ReactDOM.createRoot(wrapper);
+ root.render();
+ return { root, wrapper };
+ },
+ onRemove: elements => {
+ elements?.root.unmount();
+ elements?.wrapper.remove();
+ },
+ });
+
+ ui.mount();
+ },
+});
diff --git a/entrypoints/content/style.css b/entrypoints/content/style.css
new file mode 100644
index 0000000..acd26e8
--- /dev/null
+++ b/entrypoints/content/style.css
@@ -0,0 +1,23 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+* {
+ padding: 0;
+ margin: 0;
+}
+
+body {
+ padding: 16px;
+}
+
+.selected-search-box {
+ z-index: 300;
+ position: absolute;
+ cursor: pointer;
+ border: 0;
+ background: #fff;
+ box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.1);
+ border-radius: 6px;
+ padding: 10px 15px 9px 16px;
+}
diff --git a/jsconfig.json b/jsconfig.json
new file mode 100644
index 0000000..611a41c
--- /dev/null
+++ b/jsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@src/*": [
+ "./src/*"
+ ]
+ },
+ "jsx": "react"
+ }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index b5bd4cd..71515a6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "silo",
"private": true,
- "version": "1.3.1",
+ "version": "1.4.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/src/App.jsx b/src/App.jsx
index 85658d9..a817e4d 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -5,23 +5,29 @@ import {
Routes,
Route,
Navigate,
- BrowserRouter as Router,
+ HashRouter as Router,
} from 'react-router-dom';
import ImageGenerate from './pages/image-generate';
import { useIsImageMode } from './store/storage';
+import WebCopilot from './pages/web-copilot';
function App() {
const [isImageMode] = useIsImageMode();
return (
-
- {isImageMode ? : }
- {/*
- } />
- } />
- } />
- */}
+
+
+
+ {isImageMode ? : }
+ >
+ }
+ />
+ } />
+
);
diff --git a/src/components/CustomModelDrawer/CustomModelForm.jsx b/src/components/CustomModelDrawer/CustomModelForm.jsx
index 9990bba..50ddeba 100644
--- a/src/components/CustomModelDrawer/CustomModelForm.jsx
+++ b/src/components/CustomModelDrawer/CustomModelForm.jsx
@@ -10,6 +10,7 @@ import { useState } from 'react';
import { Button } from 'tdesign-react';
import { CUSTOM_PRESET_PREFIX } from '../../utils/types';
import { useMemo } from 'react';
+import { isBrowserExtension } from '@src/utils/utils';
const { FormItem } = Form;
const ID_REGEX = /^[a-zA-Z0-9_\-@\.]+\/[a-zA-Z0-9_\-@\.\/]+$/;
@@ -92,14 +93,14 @@ export default forwardRef((props, ref) => {
const isValid = ids.every(item => ID_REGEX.test(item));
return isValid
? Promise.resolve({
- result: true,
- })
+ result: true,
+ })
: Promise.resolve({
- result: false,
- message:
- 'ID格式错误,请检查是否符合: {manufacturer}/{model-name},多个需用英文逗号隔开',
- type: 'warning',
- });
+ result: false,
+ message:
+ 'ID格式错误,请检查是否符合: {manufacturer}/{model-name},多个需用英文逗号隔开',
+ type: 'warning',
+ });
},
},
],
@@ -208,8 +209,15 @@ export default forwardRef((props, ref) => {
>
) : (
-
+
diff --git a/src/components/InputControl.jsx b/src/components/InputControl.jsx
index aa29fb7..0df1a88 100644
--- a/src/components/InputControl.jsx
+++ b/src/components/InputControl.jsx
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { useIsMobile } from '../utils/use';
import { Popconfirm } from 'tdesign-react';
-export default function ({ onStop, onSubmit, loading }) {
+export default function ({ onStop, onSubmit, loading, enter, placeholder }) {
const [input, setInput] = useState('');
const inputRef = useRef();
const isMobile = useIsMobile();
@@ -44,7 +44,7 @@ export default function ({ onStop, onSubmit, loading }) {
};
const onKeyDown = e => {
- if (isMobile) return; // 移动端只允许点击发送
+ if (!enter && isMobile) return; // 移动端只允许点击发送
if (e.key === 'Enter') {
// 允许回车键发送
if (!e.shiftKey) {
@@ -67,9 +67,6 @@ export default function ({ onStop, onSubmit, loading }) {
(input.includes('\n') ? 'rounded-2xl' : 'rounded-3xl')
}
>
- {/*
-
- */}
{
+ onStop(true);
+ if (message) {
+ setTimeout(() => {
+ if (message.type === 'query') {
+ onSubmit(message.selection);
+ }
+ }, 16);
+ }
+ }, [message]);
+
+ const modelResponses = useLastGptResponse();
+ const [activeIndex, setActiveIndex] = useState(0);
+ const optionLength = modelResponses.length;
+ const handleKeyDown = event => {
+ setActiveIndex(prev => {
+ let target = prev;
+ if (event.key === 'ArrowLeft') {
+ target--;
+ } else if (event.key === 'ArrowRight') {
+ target++;
+ }
+ target = Math.max(0, Math.min(target, optionLength - 1));
+ return target;
+ });
+ };
+
+ useEffect(() => {
+ window.addEventListener('keydown', handleKeyDown);
+ return () => {
+ window.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [setActiveIndex, optionLength]);
+
+ if (!modelResponses.length) return null;
+ return (
+
+
+ {modelResponses[activeIndex] && (
+
+ )}
+
+
+
+
+
+ {modelResponses.map((response, index) => (
+
setActiveIndex(index)}
+ >
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/mock.json b/src/pages/mock.json
new file mode 100644
index 0000000..587fc3b
--- /dev/null
+++ b/src/pages/mock.json
@@ -0,0 +1,10 @@
+{
+ "selection": "琼脂",
+ "type": "query",
+ "context": {
+ "content": "PRIME\nMatrix\n栏目\nPi Store\n更多\n看懂配料表、营养成分表,我是这样通过包装选购食品的\n谷丰\n09/05 15:00\n前言\n\n做为一个糙汉子,在吃东西这事儿上从来都是秉持着「能吃就行」以及「来者不拒」的态度。在超市里购买食品时最多看看食品的生产日期和保质期,从来不会考虑营养成分有多少这类问题。如今家里有了娃之后,出于一个老父亲的忧思,想要了解食品营养价值这一想法开始出现在我的脑海。然而纵然商超中有专门的儿童食品货柜,琳琅满目的商品却依然使我不知该如何选择。有心比较一下,却不知如何比起——明明在食品包装上面写明了配料和营养成分,可是却不清楚这些文字代表着什么。\n\n超市货架上琳琅满目的商品\n\n于是,我开始研究如何在选购食品时对比它们的营养价值,渐渐也有了一点心得。本文将结合国家标准和自己的经验做出阐述,内容虽然也涉及一些理论,但更多是实践。\n\n食品包装上都有哪些内容\n\n国家标准 GB7718-2011《预包装食品标签通则》中规定:\n\n直接向消费者提供的预包装食品标签标示应包括食品名称、配料表、净含量和规格、生产者和(或)经销者的名称、地址和联系方式、生产日期和保质期、贮存条件、食品生产许可证编号、产品标准代号及其他需要标示的内容。\n\n国家标准 GB 28050-2011《预包装食品营养标签通则》中规定:\n\n预包装食品标签上向消费者提供食品营养信息和特性的说明,包括营养成分表、营养声称和营养成分功能声称。 营养标签是预包装食品标签的一部分。\n\n实际生活中,食品标签往往以这样的形式出现在我们眼前:\n\n蛋黄派的包装\n\n看起来似乎有些复杂,实际上只需要配料表和营养成分表这两项的内容,就可以对该食品的营养价值做出基本的判断。\n\n配料表\n\n国家标准 GB7718-2011《预包装食品标签通则》中规定:\n\n食品标签的配料表中列出了该食品在制造或加工时使用的,并存在(包括以改性的形式存在)于产品中的任何物质,包括食品添加剂。\n\n食品配料表中的各种配料应按制造或加工食品时加入量的递减顺序一一排列(加入量不超过 2% 的配料可以不按递减顺序排列)。\n\n如果某种配料是由两种或两种以上的其他配料构成的复合配料(不包括复合食品添加剂),应在配料表中标示复合配料的名称,随后将复合配料的原始配料在括号内按加入量的递减顺序标示。\n\n该如何理解这段话呢?假如我想要在同品牌同规格的两种不同酸奶中做出选择,可以这样分析它们的配料表:\n\n两种酸奶的配料表对比\n\n根据「食品配料表中的各种配料应按制造或加工食品时加入量的递减顺序一一排列」这一条,可以判断两者的主要配料都是「生牛乳」,但是 A 酸奶的生牛乳含量为 80%,B 酸奶的生牛乳含量为 98%。\n\n继续往后看,两者的配料表在第二项开始出现不同: A 酸奶配料表中第二高含量的配料是饮用水,之后才是用于改善酸奶口味的食品添加剂和各类益生菌。其中,白砂糖、果葡糖浆、安赛蜜、三氯蔗糖、食用香精用来改善酸奶口味;羟丙基二淀粉磷酸酯、琼脂用来改善酸奶黏稠度;最后三项是酸奶中添加的益生菌菌种。而 B 酸奶的配料表中,第二项就开始是食品添加剂和各类益生菌。其中,木糖醇、稀奶油、三氯蔗糖用来改善酸奶口味;羟丙基二淀粉磷酸酯、明胶、双乙酰酒石酸单双甘油酯、果胶来改善酸奶黏稠度;最后三项和 A 酸奶相同,为益生菌菌种。\n\n将两种酸奶的配料分类后,对比起来更明显\n\n看到这里就可以得出一个初步判断:两种酸奶的营养价值基本一致(主料和益生菌都相同),酸奶 A 的配料倾向于优化味道;酸奶 B 的配料更注重保持酸奶原有的风味。因此在购买时,如果想要尝试新味道就选酸奶 A ,更喜欢原味就选酸奶 B 。\n\n只是在实际生活里,配料表中虽然列出了该食品在生产过程中使用的所有生产原料,但是作为普通的消费者,不可能了解每一种原料的作用。这时候,需要结合食品标签中的「营养成分表」来帮助抉择。\n\n营养成分表\n\n国家标准 GB 28050-2011《预包装食品营养标签通则》中规定:\n\n所有预包装食品营养标签强制标示的内容包括能量、核心营养素的含量值及其占营养素参考值(NRV)的百分比。\n\n相比配料表繁复的名目,营养成分表的组成就要简单多了。国家强制标示的内容只有能量、蛋白质、脂肪、碳水化合物和钠五个项目。除此之外,其它营养素可由商家自行决定是否标示。如膳食纤维、维生素、矿物质等,这些营养素通常被标示在上述五种营养素之后。\n\n食品标签中的营养成分表\n\n营养成分表是为了帮助消费者更全面的了解食品信息,方便比较、购买食品而设立的,我将它们的主要功能及推荐摄入量整理如下:\n\n能量:人体细胞代谢的「燃料」,成人推荐摄入量约为 2400 千卡 / 天;\n蛋白质:构成人体的必要组成成分,成人推荐摄入量约为 65 克 / 天;\n脂肪:人体主要能量来源之一,成人推荐摄入量约为当日总能量的 20%-30%;\n碳水化合物:也就是糖类物质,人体主要能量来源之一,成人推荐摄入量约为当日总能量的 50%-65%;\n钠:人体必需的矿物质营养素,成人摄入量不宜超过 2000 毫克 / 天。1\n\n那么,应该怎么通过对比营养成分表来选择合适的食品呢?简单概括就是尽量选择高蛋白质、低脂肪、低钠的食品。表格中标示的「能量」是指食品中蛋白质、脂肪、碳水化合物等营养素在人体代谢中产生能量的总和,如果没有特别的需求无须单独比较。\n\n需要注意的是,营养成分表一般以每 100 克、每 100 毫升或每份含量来表示,在进行比较的时候,应该注意单位是否统一。有时表格中的某些成分会标示为「0」,但实际上「0」并不代表没有,因为按照《预包装食品营养标签通则》中的规定,当含量不超过 0.3 克时,就可以标注为「0」。\n\n如果你和我一样,不喜欢在各种数值中算来算去的话,还可以通过表格中给出的 NRV 参考值(营养素参考值百分比)来进行判断。\n\n中国食品标签营养素参考值(Nutrient Reference Values,NRV,以下简称「营养素参考值」)是食品营养标签上比较食品营养素含量多少的参考标准,是消费者选择食品时的一种营养参照尺度。 营养素参考值依据我国居民膳食营养素推荐摄入量(RNI)和适宜摄入量(AI)而制定。\n\n通过这个百分比可以了解到食用 100 克(100 毫升或每份食物)该食物能够满足多少自己身体一天的营养需求。如果食物营养成分表中标示的「钠」的 NRV 是 8% ,代表着每 100 克该食物将提供成年人每日推荐摄入总量 8% 的钠。因此也可以通过比较 NRV 的数值来判断食物的营养价值。\n\n例如上文提到的两款酸奶:\n\n因为是奶制品,就以比较两者营养成分表中「钙」含量(每 100 克)为例:\n\n酸奶 A 的 NRV 值是 10%,可以提供成年人每日推荐摄入总量 10% 的「钙」;\n酸奶 B 的 NRV 值是 13%,可以提供成年人每日推荐摄入总量 13% 的「钙」;\n\n通过对比就可以看出,酸奶 B 的钙含量一定是更高的。\n\n购买实践\n\n理论讲到这里其实就差不多了,但「实践才是检验真理的唯一标准」,而且最佳的食物选择会因每人的年龄、体重、运动量、身体状况等情况的不同而不同。有时营养成分高的食品,未必是适合自己的。因此我特意去超市里面寻找了一些常见食品,拍下它们的标签来作为实践的例子。\n\n实践一\n\n为了模拟真实的情况,我假设一个场景作为前提:炎热的夏天,你顶着烈日终于找到了一个小超市,你抹了把脸上的汗,看到货架上面摆放了饮料 A 和饮料 B ,他们的包装标签如下:\n\n先看它们的配料表,根据「食品配料表中的各种配料应按制造或加工食品时加入量的递减顺序一一排列」这一原则,判断出:\n\nA 饮料的主要成分是:水、白砂糖、浓缩西柚汁、果葡糖浆;\nB 饮料的主要成分是:纯净水、浓缩西柚汁、浓缩梨汁、金银花浓缩汁、浓缩柠檬汁。\n\n因为大量出汗后人体应补充水分和钠,所以查看两种饮料的营养成分表时,优先对比钠的含量。注意,两者的计算单位不同,需要将 A 饮料的数值换算成每份 100 毫升再进行对比:\n\nA 饮料的钠换算后约 27 毫克;\nB 饮料的钠是 16 毫克。\n\n比较过后可以得出结论:出汗过后的你,更适合饮用饮料 A 。饮料 A 不仅量大(一瓶445ml)钠的含量也比饮料 B 高,可以快速的补充身体所需,所以它是更合适的选择。如果脱离假设的的场景,只是嘴馋了,饮料 B 就是更健康的选择(添加剂少、钠的含量也低)。\n\n实践二\n\n再假设一个场景:你刚吃过了早饭,感觉肚子里还差点意思,想再吃点饼干做个收尾。因为只是零食,所以你想做出一个相对健康的选择:\n\n依然先看配料表:\n\nA 饼干的主要成分是:小麦粉、白砂糖、食用植物油;\nB 饼干的主要成分是:白砂糖,小麦粉,蛋白液。\n\n再看营养成分表(此时建议看 NRV 值,因为单看含量数值只能分高低,并不能反应营养素的摄入量是否会超过每日的推荐摄入总量)。表格里面的前四项数值差距并不大,唯有 B 饼干钠的 NRV 数值比 A 饼干高了 10% 。结合配料表给出的主要成分可以判断,B 饼干属于高糖高盐的食物(因为配料表中的白砂糖排在了小麦粉的前面,意味着白砂糖的含量比小麦粉要高)。如果食用 B 饼干,再加上午饭和晚饭,不注意的话很容易会摄入过量的盐和糖,因此食用 A 饼干是更好的选择。\n\n关于食品添加剂\n\n我最后再表达一些关于食品添加剂的个人见解。国家对食品添加剂的使用做出了详细规定(《食品添加剂使用标准》GB2760—2024 ),食品添加剂在允许使用的范围内通常是安全的。如果非要问食品添加剂有没有毒性,我肯定会回答有毒。但是抛开剂量谈毒性无异于耍流氓。要知道,喝水都能中毒呢(水中毒 Water intoxication )。\n\n因此我特意找了一些有关食品添加剂致死量的数据:比如硝酸钠(硝酸盐),人的致死量为15.3g。如果用它制作肉食,国家规定的最大使用量是 0.5 克 / 公斤,也就是说,得一口气吃 60 多斤的肉食才会致死——这是完全超出常理的事。当然,选择无添加的食品更为健康,但是我认为完全没有必要谈之色变,理性看待即可。\n\n结语\n\n自从家里有了娃之后,开始了解起了许多未曾关注地方。我也是在经过了一段时间的不习惯之后才养成了对比食品包装的习惯。如果您对此感兴趣的话,也可以在日常购物时多花一些时间查看和比较不同产品的包装,相信很快就能更快速准确地解读包装上的信息了,进而为自己和家人做出更健康的食品选择。\n\n最后再叠个甲,我并非食品安全领域的专业人士。就像开头说的,最开始研究它的目的不过是想给娃买点零食,渐渐有了一点心得,最终才有了本文。因此如有疏漏之处还请指正,万分感谢!\n\n参考资料:\n预包装食品营养标签通则(GB 28050-2011)\n预包装食品标签通则(GB7718-2011)\n食品营养标签管理规范(卫监督发[2007]300号)\n食品添加剂使用标准(GB 2760-2024)\n中国居民膳食指南(2022)\n\n> 关注 少数派小红书,感受精彩数字生活 🍃\n\n> 实用、好用的 正版软件,少数派为你呈现 🚀\n\n1\n上述数值仅供参考,摘取自《中国居民膳食指南(2022)》。最佳摄入量因个人的年龄、性别、体重、活动量的不同而不同。\n71\n19\n前言\n食品包装上都有哪些内容\n配料表\n营养成分表\n购买实践\n实践一\n实践二\n关于食品添加剂\n结语\n本文责编:@Lotta\n© 本文著作权归作者所有,并授权少数派独家使用,未经少数派许可,不得转载使用。\n#生活方式#购物\n 71\n少数派93096903、vovinsins、KElee01 等 71 人为本文章充电\n谷丰\n吃饱喝好,心情好好\n关注\n全部评论(19)\n热门排序\n请在登录后评论...\nFFFuture\n09/05 15:55\n如果你想单纯的看热量高不高,那就用能量的值除以4,因为千焦和大卡的换算差不多是4倍,如果你还想控制脂肪的摄入那就看碳脂比,如果小于5:1,还是考虑考虑再吃,因为它是热量炸弹———来自一个减脂人群的小技巧\n1\n20\n埃策尔\n09/05 15:56\n学到了\n0\n6qishi\n09/05 16:48\n直接把两个产品的配料表都拍下来,发给AI对比一下可行吗,感觉有搞头\n2\n15\n谷丰\n09/05 19:03\n这个想法不错,如果有的话对比起来会方便许多。\n3\n我是一只特立独行的猪\n09/06 08:47\n这个比试图看热量来的更清晰准确的,识别文字上目前已经很成熟,后面就是归纳总结了。是个很好想法!\n1\n少数派太少数了\n09/05 15:23\n有个疑问想咨询一下,在 实践一 里的 A产品。 影响成分写了 每份是445mL。那么他后面的 NRV%,是说这一瓶 445mL的饮料, 能提供8%的能量,还是说依然是 固定单位100mL 的能量。\n2\n6\n谷丰\n09/05 16:29\n表里标注的是每份 445ml 的 NRV%。就是说你在喝光饮料的情况下,摄入的能量占国家推荐每日摄入量的 8%。\n4\n少数派太少数了\n回复\n谷丰\n09/05 22:26\n谢谢!这样子就没那么迷茫了\n0\nEhnap\n09/05 17:15\n遇上配料表造假的就没辙了……\n3\n2\n谷丰\n09/05 19:01\n配料表造假......这是违法了吧😂\n0\nZLNAEEZD\n回复\n谷丰\n09/06 10:29\n1、大厂按理说比三无小厂和路边摊靠谱一些\n\n2、但大厂也确实经常坑蒙拐骗爆出不合格,无论是小的抽检不合格新闻,还是大规模事件三路、算才等等。大家其实只是选择闭着眼欺骗自己而已,不然咋办?一家不靠谱可以选另一家,家家不靠谱我们也不能饿死啊,只能选择闭着眼。\n\n3、没人管的小厂、路边摊更是选择闭着眼。大厂都不遵守的规则,路边摊那添...展开\n0\nEhnap\n回复\n谷丰\n昨天 15:03\n就是个成本问题,只能说大厂造假概率低,但是之前也有爆出过盒马乱贴配料表的,但是声音立刻就消失了,emmm……\n0\n仓陈雍古\n09/06 20:50\n人过中年,买食品时候除了鲜奶这种短保质期的,其他包装类食物都不咋看,一般最多临期,如果过期我还得谢谢超市呢……\n\n另外,我没事喜欢去盒马奥莱买临期品。\n\n分享一个小细节:配料表越长的越谨慎购买,配料表的成分顺序就是成品的含量顺序。比如我在买面包的时候,看到白砂糖在面粉之后,水和鸡蛋的前面,我就知道这面包没啥必要买了\n\n根据我这几年...展开\n2\n1\nPaperWing\n09/07 08:05\n看了下手里的,基本带甜味的白砂糖就在前三\n0\n谷丰\n09/07 08:43\n是这样的哎,现在市售的大部分食品和饮料仔细一看都有糖,即使宣传零糖的食品也基本还是有代糖......虽然说为了口味也可以理解,但同样作为中年人,还是能少吃就少吃点吧。\n0\nAllen1\n09/06 17:47\n学会了\n0\n1\n潘誉晗\n09/06 13:23\n太专业了。\n0\n0\n青南\n09/06 12:20\n配料表是乱写的。\n1\n0\nPaperWing\n09/06 14:28\n相关人士?\n2\n没有更多评论了哦\n推荐阅读\n我的购房指北:买房之前可以关注什么?\nWannz\n127\n本周看什么 | 最近值得一看的 7 部作品\n少数派编辑部\n10\n仅靠软装,我打造了一个「轻度极繁主义」的家\nElijahLee\n195\n本周看什么 | 最近值得一看的 8 部作品\n少数派编辑部\n17\n玄学 or 科学?MBTI 测试准不准、对你的生活有影响吗?\nMatrix机器人\n31\n本周看什么 | 最近值得一看的 7 部作品\n少数派编辑部\n21\n \n下载 App\n联系我们\n商务合作\n关于我们\n用户协议\n© 2013-2024 少数派粤ICP备09128966号-4 | 粤B2-20211534",
+ "title": "看懂配料表、营养成分表,我是这样通过包装选购食品的 - 少数派",
+ "keywords": "生活方式,购物",
+ "description": "前言做为一个糙汉子,在吃东西这事儿上从来都是秉持着「能吃就行」以及「来者不拒」的态度。在超市里购买食品时最多看看食品的生产日期和保质期,从来不会考虑营养成分有多少这类问题。如今家里有了娃之后,出于一个 ..."
+ }
+}
\ No newline at end of file
diff --git a/src/pages/web-copilot.jsx b/src/pages/web-copilot.jsx
new file mode 100644
index 0000000..442d878
--- /dev/null
+++ b/src/pages/web-copilot.jsx
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+import mockData from './mock.json';
+import { useEffect } from 'react';
+import WebCopilot from '@src/components/WebCopilot';
+import { isBrowserExtension } from '@src/utils/utils';
+export default function () {
+ const [message, setMessage] = useState(!isBrowserExtension ? mockData : null);
+
+ useEffect(() => {
+ const handleMessage = event => {
+ if (event.data && typeof event.data === 'string') {
+ console.log(event.data);
+ setMessage(JSON.parse(event.data));
+ }
+ };
+
+ window.addEventListener('message', handleMessage);
+ return () => {
+ window.removeEventListener('message', handleMessage);
+ };
+ }, []);
+ return (
+
+ {message && }
+
+ );
+}
diff --git a/src/services/prompt/web-copilot.txt b/src/services/prompt/web-copilot.txt
new file mode 100644
index 0000000..c5b7fb6
--- /dev/null
+++ b/src/services/prompt/web-copilot.txt
@@ -0,0 +1,9 @@
+Web Assistant
+I will provide you with some contextual information. Please **combine the contextual information** and your knowledge base to answer the user's questions
+
+### Requirements
+1. Please use Chinese to communicate (even if the page is in other languages), some professional terms can remain in English !!!
+2. Your main role is to explain, no dialogue is required
+3. Please use simple language as much as possible while keeping the information intact
+
+### The context is as follows
\ No newline at end of file
diff --git a/src/utils/utils.js b/src/utils/utils.js
index 9f71ad4..1e8af3a 100644
--- a/src/utils/utils.js
+++ b/src/utils/utils.js
@@ -82,4 +82,6 @@ export function streamChat (model, messages, controller, onChunk, onEnd, onError
const modelChatOptions = getChatRequestOptions(model)
const resolver = getChatResolver(model);
return resolver(model, messages, modelChatOptions, controller, onChunk, onEnd, onError)
-}
\ No newline at end of file
+}
+
+export const isBrowserExtension = !!import.meta.env.BROWSER
diff --git a/tailwind.config.js b/tailwind.config.js
index e8d3c9f..029ea7f 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -6,6 +6,7 @@ export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
+ "./entrypoints/**/*.{js,ts,jsx,tsx}"
],
darkMode: 'selector',
theme: {
diff --git a/vite.config.js b/vite.config.js
index 4c71434..74aaa94 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,4 +1,5 @@
import { defineConfig } from 'vite'
+import path from 'path'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
@@ -6,7 +7,7 @@ export default defineConfig({
plugins: [react()],
resolve: {
alias: {
- '@': '/src'
+ '@src': path.resolve(__dirname, './src'),
}
}
})
diff --git a/wxt.config.js b/wxt.config.js
index 0c13cf1..bbdfd41 100644
--- a/wxt.config.js
+++ b/wxt.config.js
@@ -6,7 +6,14 @@ export default defineConfig({
modules: ['@wxt-dev/module-react'],
manifest: {
name: 'Silo - SiliconCloud API Playground',
+ permissions: [],
action: {},
+ "web_accessible_resources": [
+ {
+ "resources": ["ext.html"],
+ "matches": ['*://*/*']
+ }
+ ]
},
vite: () => ({
esbuild: {
@@ -14,4 +21,10 @@ export default defineConfig({
drop: ['debugger'], // 删除 debugger
},
}),
+ alias: {
+ '@src': path.resolve(__dirname, './src'),
+ },
+ runner: {
+ startUrls: ["https://developer.mozilla.org/en-US/docs/Web/CSS/perspective"],
+ },
});
\ No newline at end of file