mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-10 23:48:26 +00:00
Fix: show credits in the dropdown
This commit is contained in:
parent
2e08470a03
commit
b0ff41823e
33
apps/extension/.gitignore
vendored
33
apps/extension/.gitignore
vendored
@ -1,33 +0,0 @@
|
||||
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
out/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# plasmo
|
||||
.plasmo
|
||||
|
||||
# typescript
|
||||
.tsbuildinfo
|
||||
@ -1,26 +0,0 @@
|
||||
/**
|
||||
* @type {import('prettier').Options}
|
||||
*/
|
||||
export default {
|
||||
printWidth: 80,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: false,
|
||||
singleQuote: false,
|
||||
trailingComma: "none",
|
||||
bracketSpacing: true,
|
||||
bracketSameLine: true,
|
||||
plugins: ["@ianvs/prettier-plugin-sort-imports"],
|
||||
importOrder: [
|
||||
"<BUILTIN_MODULES>", // Node.js built-in modules
|
||||
"<THIRD_PARTY_MODULES>", // Imports not matched by other special words or groups.
|
||||
"", // Empty line
|
||||
"^@plasmo/(.*)$",
|
||||
"",
|
||||
"^@plasmohq/(.*)$",
|
||||
"",
|
||||
"^~(.*)$",
|
||||
"",
|
||||
"^[./]"
|
||||
]
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
This is a [Plasmo extension](https://docs.plasmo.com/) project bootstrapped with [`plasmo init`](https://www.npmjs.com/package/plasmo).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
# or
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open your browser and load the appropriate development build. For example, if you are developing for the chrome browser, using manifest v3, use: `build/chrome-mv3-dev`.
|
||||
|
||||
You can start editing the popup by modifying `popup.tsx`. It should auto-update as you make changes. To add an options page, simply add a `options.tsx` file to the root of the project, with a react component default exported. Likewise to add a content page, add a `content.ts` file to the root of the project, importing some module and do some logic, then reload the extension on your browser.
|
||||
|
||||
For further guidance, [visit our Documentation](https://docs.plasmo.com/)
|
||||
|
||||
## Making production build
|
||||
|
||||
Run the following:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# or
|
||||
npm run build
|
||||
```
|
||||
|
||||
This should create a production bundle for your extension, ready to be zipped and published to the stores.
|
||||
|
||||
## Submit to the webstores
|
||||
|
||||
The easiest way to deploy your Plasmo extension is to use the built-in [bpp](https://bpp.browser.market) GitHub action. Prior to using this action however, make sure to build your extension and upload the first version to the store to establish the basic credentials. Then, simply follow [this setup instruction](https://docs.plasmo.com/framework/workflows/submit) and you should be on your way for automated submission!
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB |
@ -1,129 +0,0 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
interface InputSuggestionsPopupProps {
|
||||
facts: string[]
|
||||
isLoading: boolean
|
||||
onFactSelect: (fact: string) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function InputSuggestionsPopup({
|
||||
facts,
|
||||
isLoading,
|
||||
onFactSelect,
|
||||
onClose
|
||||
}: InputSuggestionsPopupProps) {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
|
||||
|
||||
// Auto-hide after 10 seconds
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
onClose()
|
||||
}, 10000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [onClose])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: "white",
|
||||
border: "1px solid #ddd",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, sans-serif",
|
||||
fontSize: "14px",
|
||||
maxWidth: "400px",
|
||||
maxHeight: "300px",
|
||||
overflowY: "auto"
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "12px",
|
||||
fontWeight: "500",
|
||||
color: "#333",
|
||||
borderBottom: "1px solid #eee",
|
||||
paddingBottom: "8px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<span>AI Suggestions</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onClose()
|
||||
}}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "#6B7280",
|
||||
cursor: "pointer",
|
||||
fontSize: "20px",
|
||||
padding: "0",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ marginBottom: "12px" }}>
|
||||
{isLoading ? (
|
||||
<div
|
||||
style={{
|
||||
color: "#666",
|
||||
fontSize: "13px",
|
||||
textAlign: "center",
|
||||
padding: "20px"
|
||||
}}>
|
||||
Searching for relevant facts...
|
||||
</div>
|
||||
) : facts.length > 0 ? (
|
||||
facts.slice(0, 5).map((fact, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => onFactSelect(fact)}
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
margin: "4px 0",
|
||||
background: hoveredIndex === index ? "#e9ecef" : "#f8f9fa",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
border: "1px solid #e9ecef",
|
||||
transition: "background-color 0.2s"
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
color: "#333",
|
||||
lineHeight: "1.4"
|
||||
}}>
|
||||
{fact}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
color: "#666",
|
||||
fontSize: "13px",
|
||||
textAlign: "center",
|
||||
padding: "20px"
|
||||
}}>
|
||||
No relevant suggestions found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,166 +0,0 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
interface TextSelectionPopupProps {
|
||||
selectedText: string
|
||||
onSave: (text: string) => Promise<void>
|
||||
onClose: () => void
|
||||
isSaving: boolean
|
||||
saveStatus: "idle" | "saving" | "success" | "error" | "empty"
|
||||
}
|
||||
|
||||
export default function TextSelectionPopup({
|
||||
selectedText,
|
||||
onSave,
|
||||
onClose,
|
||||
isSaving,
|
||||
saveStatus
|
||||
}: TextSelectionPopupProps) {
|
||||
const [text, setText] = useState(selectedText)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
// Focus textarea on mount
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.focus()
|
||||
textareaRef.current.setSelectionRange(
|
||||
textareaRef.current.value.length,
|
||||
textareaRef.current.value.length
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSave = async () => {
|
||||
const textToSave = text.trim()
|
||||
if (!textToSave) {
|
||||
return
|
||||
}
|
||||
await onSave(textToSave)
|
||||
}
|
||||
|
||||
const getButtonText = () => {
|
||||
switch (saveStatus) {
|
||||
case "saving":
|
||||
return "Saving..."
|
||||
case "success":
|
||||
return "Saved!"
|
||||
case "error":
|
||||
return "Error"
|
||||
case "empty":
|
||||
return "No text to save"
|
||||
default:
|
||||
return "Add to Memory"
|
||||
}
|
||||
}
|
||||
|
||||
const getButtonColor = () => {
|
||||
switch (saveStatus) {
|
||||
case "success":
|
||||
return "#10B981"
|
||||
case "error":
|
||||
case "empty":
|
||||
return "#EF4444"
|
||||
default:
|
||||
return "#c15e50"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
style={{
|
||||
background: "white",
|
||||
border: "1px solid #ddd",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, sans-serif",
|
||||
fontSize: "14px",
|
||||
width: "350px"
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "12px",
|
||||
fontWeight: "500",
|
||||
color: "#333",
|
||||
borderBottom: "1px solid #eee",
|
||||
paddingBottom: "8px"
|
||||
}}>
|
||||
Add to Core Memory
|
||||
</div>
|
||||
|
||||
{/* Textarea */}
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
placeholder="Edit the text you want to save to memory..."
|
||||
style={{
|
||||
width: "100%",
|
||||
minHeight: "80px",
|
||||
maxHeight: "150px",
|
||||
resize: "vertical",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
background: "#f8f9fa",
|
||||
borderRadius: "4px",
|
||||
padding: "8px",
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, sans-serif",
|
||||
fontSize: "13px",
|
||||
lineHeight: "1.4",
|
||||
color: "#333",
|
||||
marginBottom: "12px",
|
||||
boxSizing: "border-box"
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Buttons */}
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || !text.trim()}
|
||||
style={{
|
||||
flex: 1,
|
||||
background: getButtonColor(),
|
||||
color: "white",
|
||||
border: "none",
|
||||
padding: "8px 16px",
|
||||
borderRadius: "4px",
|
||||
cursor: isSaving || !text.trim() ? "not-allowed" : "pointer",
|
||||
fontSize: "13px",
|
||||
opacity: isSaving || !text.trim() ? 0.6 : 1
|
||||
}}>
|
||||
{getButtonText()}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: "#6B7280",
|
||||
color: "white",
|
||||
border: "none",
|
||||
padding: "8px 16px",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
fontSize: "13px"
|
||||
}}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,260 +0,0 @@
|
||||
export interface LogoProps {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export default function StaticLogo({ width, height }: LogoProps) {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 282 282"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M80.0827 34.974C92.7457 3.8081 120.792 17.7546 134.476 29.5676C135.325 30.301 135.792 31.3761 135.792 32.4985V250.806C135.792 251.98 135.258 253.117 134.349 253.858C103.339 279.155 85.2835 259.158 80.0827 245.771C44.9187 241.11 43.965 209.932 47.8837 194.925C15.173 187.722 17.5591 152.731 22.841 136.135C9.34813 107.747 33.9141 90.4097 47.8837 85.2899C40.524 50.5456 66.2831 37.2692 80.0827 34.974Z"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="95.4357"
|
||||
y2="-2.5"
|
||||
transform="matrix(0.591888 0.80602 -0.783494 0.6214 77.3574 37.2551)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<path
|
||||
d="M49.1309 84.7972L136.212 176.505"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="90.8224"
|
||||
y2="-2.5"
|
||||
transform="matrix(0.814972 0.5795 -0.549892 0.835235 32.5566 143.53)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="146.141"
|
||||
y2="-2.5"
|
||||
transform="matrix(0.686415 -0.72721 0.700262 0.713886 35.4785 139.482)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="77.4689"
|
||||
y2="-2.5"
|
||||
transform="matrix(0.528012 -0.849237 0.830187 0.557485 49.1133 196.162)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="111.328"
|
||||
y2="-2.5"
|
||||
transform="matrix(-0.979793 0.200014 -0.185721 -0.982602 135.791 117.215)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="58.5744"
|
||||
y2="-2.5"
|
||||
transform="matrix(0.532065 -0.846704 0.827428 0.561572 81.252 246.769)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="64.5426"
|
||||
y2="-2.5"
|
||||
transform="matrix(-0.503861 -0.863784 0.846088 -0.533044 137.443 252.884)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="45.7129"
|
||||
y2="-2.5"
|
||||
transform="matrix(-0.0852204 0.996362 -0.99576 -0.0919863 110.471 150.615)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="87.7453"
|
||||
y2="-2.5"
|
||||
transform="matrix(0.998935 -0.0461398 0.0427267 0.999087 49.1133 198.186)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="64.6589"
|
||||
y2="-2.5"
|
||||
transform="matrix(-0.165686 0.986178 -0.983932 -0.178541 100.73 66.6073)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<circle cx="103.955" cy="66.3968" r="11.0444" fill="#C15E50" />
|
||||
<circle cx="90.4996" cy="129.403" r="11.5465" fill="#C15E50" />
|
||||
<circle cx="103.955" cy="197.449" r="11.0444" fill="#C15E50" />
|
||||
<circle cx="48.4992" cy="86.0547" r="8.53434" fill="#C15E50" />
|
||||
<path
|
||||
d="M86.0294 34.6113C86.0294 38.7701 82.6579 42.1416 78.4991 42.1416C74.3402 42.1416 70.9688 38.7701 70.9688 34.6113C70.9688 30.4524 74.3402 27.081 78.4991 27.081C82.6579 27.081 86.0294 30.4524 86.0294 34.6113Z"
|
||||
fill="#C15E50"
|
||||
/>
|
||||
<circle cx="29.0525" cy="140.996" r="13.0525" fill="#C15E50" />
|
||||
<circle cx="79.0009" cy="246.875" r="7.02828" fill="#C15E50" />
|
||||
<path
|
||||
d="M53.0314 195.433C53.0314 199.869 49.4352 203.466 44.9991 203.466C40.563 203.466 36.9668 199.869 36.9668 195.433C36.9668 190.997 40.563 187.401 44.9991 187.401C49.4352 187.401 53.0314 190.997 53.0314 195.433Z"
|
||||
fill="#C15E50"
|
||||
/>
|
||||
<path
|
||||
d="M202.806 247.026C190.143 278.192 162.097 264.245 148.413 252.432C147.563 251.699 147.097 250.624 147.097 249.501V31.1935C147.097 30.0203 147.631 28.8833 148.54 28.1417C179.549 2.84476 197.605 22.8418 202.806 36.2294C237.97 40.8903 238.924 72.0684 235.005 87.0748C267.716 94.2779 265.33 129.269 260.048 145.865C273.541 174.253 248.975 191.59 235.005 196.71C242.365 231.454 216.606 244.731 202.806 247.026Z"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="95.4357"
|
||||
y2="-2.5"
|
||||
transform="matrix(-0.591888 -0.80602 0.783494 -0.6214 205.531 244.745)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<path
|
||||
d="M233.758 197.203L146.677 105.495"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="90.8224"
|
||||
y2="-2.5"
|
||||
transform="matrix(-0.814972 -0.5795 0.549892 -0.835235 250.332 138.47)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="146.141"
|
||||
y2="-2.5"
|
||||
transform="matrix(-0.686415 0.72721 -0.700262 -0.713886 247.41 142.518)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="77.4689"
|
||||
y2="-2.5"
|
||||
transform="matrix(-0.528012 0.849237 -0.830187 -0.557485 233.775 85.838)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="111.328"
|
||||
y2="-2.5"
|
||||
transform="matrix(0.979793 -0.200014 0.185721 0.982602 147.098 164.785)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="58.5744"
|
||||
y2="-2.5"
|
||||
transform="matrix(-0.532065 0.846704 -0.827428 -0.561572 201.637 35.2307)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="64.5426"
|
||||
y2="-2.5"
|
||||
transform="matrix(0.503861 0.863784 -0.846088 0.533044 145.445 29.1161)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="45.7129"
|
||||
y2="-2.5"
|
||||
transform="matrix(0.0852204 -0.996362 0.99576 0.0919863 172.418 131.385)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="87.7453"
|
||||
y2="-2.5"
|
||||
transform="matrix(-0.998935 0.0461398 -0.0427267 -0.999087 233.775 83.8137)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<line
|
||||
y1="-2.5"
|
||||
x2="64.6589"
|
||||
y2="-2.5"
|
||||
transform="matrix(0.165686 -0.986178 0.983932 0.178541 182.158 215.393)"
|
||||
stroke="#C15E50"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<circle
|
||||
cx="178.934"
|
||||
cy="215.603"
|
||||
r="11.0444"
|
||||
transform="rotate(180 178.934 215.603)"
|
||||
fill="#C15E50"
|
||||
/>
|
||||
<circle
|
||||
cx="192.389"
|
||||
cy="152.597"
|
||||
r="11.5465"
|
||||
transform="rotate(180 192.389 152.597)"
|
||||
fill="#C15E50"
|
||||
/>
|
||||
<circle
|
||||
cx="178.934"
|
||||
cy="84.5506"
|
||||
r="11.0444"
|
||||
transform="rotate(180 178.934 84.5506)"
|
||||
fill="#C15E50"
|
||||
/>
|
||||
<circle
|
||||
cx="234.389"
|
||||
cy="195.945"
|
||||
r="8.53434"
|
||||
transform="rotate(180 234.389 195.945)"
|
||||
fill="#C15E50"
|
||||
/>
|
||||
<path
|
||||
d="M196.859 247.389C196.859 243.23 200.231 239.858 204.39 239.858C208.548 239.858 211.92 243.23 211.92 247.389C211.92 251.548 208.548 254.919 204.39 254.919C200.231 254.919 196.859 251.548 196.859 247.389Z"
|
||||
fill="#C15E50"
|
||||
/>
|
||||
<circle
|
||||
cx="253.836"
|
||||
cy="141.004"
|
||||
r="13.0525"
|
||||
transform="rotate(180 253.836 141.004)"
|
||||
fill="#C15E50"
|
||||
/>
|
||||
<circle
|
||||
cx="203.888"
|
||||
cy="35.1255"
|
||||
r="7.02828"
|
||||
transform="rotate(180 203.888 35.1255)"
|
||||
fill="#C15E50"
|
||||
/>
|
||||
<path
|
||||
d="M229.857 86.5668C229.857 82.1307 233.453 78.5345 237.89 78.5345C242.326 78.5345 245.922 82.1307 245.922 86.5668C245.922 91.0029 242.326 94.5991 237.89 94.5991C233.453 94.5991 229.857 91.0029 229.857 86.5668Z"
|
||||
fill="#C15E50"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@ -1,309 +0,0 @@
|
||||
import { createElement } from "react"
|
||||
import { createRoot, type Root } from "react-dom/client"
|
||||
|
||||
import InputSuggestionsPopup from "~components/InputSuggestionsPopup"
|
||||
import { searchFacts } from "~utils/api"
|
||||
import { isExtensionContextValid } from "~utils/context"
|
||||
|
||||
let inputDots: Map<HTMLElement, HTMLDivElement> = new Map()
|
||||
let inputPopups: Map<HTMLElement, HTMLDivElement> = new Map()
|
||||
let reactRoots: Map<HTMLElement, Root> = new Map()
|
||||
let currentUrl = window.location.href
|
||||
|
||||
// Track page navigation and reset popups
|
||||
function checkPageNavigation() {
|
||||
if (window.location.href !== currentUrl) {
|
||||
console.log("Page navigation detected, resetting input popups")
|
||||
currentUrl = window.location.href
|
||||
clearAllInputElements()
|
||||
}
|
||||
}
|
||||
|
||||
// Input field monitoring
|
||||
function createInputDot(input: HTMLElement, rect: DOMRect) {
|
||||
if (inputDots.has(input)) return
|
||||
|
||||
const dot = document.createElement("div")
|
||||
dot.style.cssText = `
|
||||
position: absolute;
|
||||
width: 54px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
z-index: 10001;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
|
||||
background: #c15e50;
|
||||
border-radius: 4px;
|
||||
padding: 3px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
// Add the logo SVG with white color and Core text - smaller version for input fields
|
||||
dot.innerHTML = `
|
||||
<div style="display: flex; flex-direction: row; align-items: center; gap: 5px;">
|
||||
<svg width="16" height="16" viewBox="0 0 282 282" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M80.0827 34.974C92.7457 3.8081 120.792 17.7546 134.476 29.5676C135.325 30.301 135.792 31.3761 135.792 32.4985V250.806C135.792 251.98 135.258 253.117 134.349 253.858C103.339 279.155 85.2835 259.158 80.0827 245.771C44.9187 241.11 43.965 209.932 47.8837 194.925C15.173 187.722 17.5591 152.731 22.841 136.135C9.34813 107.747 33.9141 90.4097 47.8837 85.2899C40.524 50.5456 66.2831 37.2692 80.0827 34.974Z" stroke="white" stroke-width="5"/>
|
||||
<circle cx="103.955" cy="66.3968" r="11.0444" fill="white"/>
|
||||
<circle cx="90.4996" cy="129.403" r="11.5465" fill="white"/>
|
||||
<circle cx="103.955" cy="197.449" r="11.0444" fill="white"/>
|
||||
<circle cx="48.4992" cy="86.0547" r="8.53434" fill="white"/>
|
||||
<path d="M202.806 247.026C190.143 278.192 162.097 264.245 148.413 252.432C147.563 251.699 147.097 250.624 147.097 249.501V31.1935C147.097 30.0203 147.631 28.8833 148.54 28.1417C179.549 2.84476 197.605 22.8418 202.806 36.2294C237.97 40.8903 238.924 72.0684 235.005 87.0748C267.716 94.2779 265.33 129.269 260.048 145.865C273.541 174.253 248.975 191.59 235.005 196.71C242.365 231.454 216.606 244.731 202.806 247.026Z" stroke="white" stroke-width="5"/>
|
||||
<circle cx="178.934" cy="215.603" r="11.0444" transform="rotate(180 178.934 215.603)" fill="white"/>
|
||||
<circle cx="192.389" cy="152.597" r="11.5465" transform="rotate(180 192.389 152.597)" fill="white"/>
|
||||
<circle cx="178.934" cy="84.5506" r="11.0444" transform="rotate(180 178.934 84.5506)" fill="white"/>
|
||||
<circle cx="234.389" cy="195.945" r="8.53434" transform="rotate(180 234.389 195.945)" fill="white"/>
|
||||
</svg>
|
||||
<div style="
|
||||
color: white;
|
||||
font-size: 8px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
">Core</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
dot.style.top = `${rect.top + window.scrollY + 2}px`
|
||||
dot.style.left = `${rect.right + window.scrollX - 24}px`
|
||||
|
||||
dot.addEventListener("click", () => showInputPopup(input))
|
||||
dot.addEventListener("mouseenter", () => showInputPopup(input))
|
||||
|
||||
document.body.appendChild(dot)
|
||||
inputDots.set(input, dot)
|
||||
|
||||
setTimeout(() => {
|
||||
dot.style.opacity = "1"
|
||||
}, 10)
|
||||
}
|
||||
|
||||
// Facts popup for input fields with React
|
||||
async function showInputPopup(input: HTMLElement) {
|
||||
if (!isExtensionContextValid()) {
|
||||
console.log("Extension context invalid, skipping input popup")
|
||||
return
|
||||
}
|
||||
|
||||
// Get value from input element, handling both input/textarea and contenteditable
|
||||
let query = ""
|
||||
if ("value" in input && (input as HTMLInputElement).value !== undefined) {
|
||||
query = (input as HTMLInputElement).value.trim()
|
||||
} else {
|
||||
query = input.textContent?.trim() || ""
|
||||
}
|
||||
|
||||
if (!query || inputPopups.has(input)) return
|
||||
|
||||
// Create popup container
|
||||
const popup = document.createElement("div")
|
||||
popup.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 10002;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
`
|
||||
|
||||
// Position popup
|
||||
const rect = input.getBoundingClientRect()
|
||||
let top = rect.bottom + 5
|
||||
let left = rect.left
|
||||
|
||||
if (left + 400 > window.innerWidth) {
|
||||
left = window.innerWidth - 410
|
||||
}
|
||||
if (left < 10) {
|
||||
left = 10
|
||||
}
|
||||
|
||||
if (top + 300 > window.innerHeight) {
|
||||
top = rect.top - 310
|
||||
}
|
||||
|
||||
popup.style.top = `${top}px`
|
||||
popup.style.left = `${left}px`
|
||||
|
||||
document.body.appendChild(popup)
|
||||
inputPopups.set(input, popup)
|
||||
|
||||
// Create React root
|
||||
const root = createRoot(popup)
|
||||
reactRoots.set(input, root)
|
||||
|
||||
// Handle fact selection
|
||||
const handleFactSelect = (factText: string) => {
|
||||
if (input) {
|
||||
// Handle both input/textarea and contenteditable elements
|
||||
if ("value" in input && (input as HTMLInputElement).value !== undefined) {
|
||||
const inputEl = input as HTMLInputElement
|
||||
inputEl.value += (inputEl.value ? " " : "") + factText
|
||||
inputEl.dispatchEvent(new Event("input", { bubbles: true }))
|
||||
} else {
|
||||
// For contenteditable elements
|
||||
const currentText = input.textContent || ""
|
||||
input.textContent = currentText + (currentText ? " " : "") + factText
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }))
|
||||
}
|
||||
removeInputPopup(input)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle popup close
|
||||
const handleClose = () => {
|
||||
removeInputPopup(input)
|
||||
}
|
||||
|
||||
// Render loading state initially
|
||||
root.render(
|
||||
createElement(InputSuggestionsPopup, {
|
||||
facts: [],
|
||||
isLoading: true,
|
||||
onFactSelect: handleFactSelect,
|
||||
onClose: handleClose
|
||||
})
|
||||
)
|
||||
|
||||
// Show popup
|
||||
setTimeout(() => {
|
||||
popup.style.opacity = "1"
|
||||
}, 10)
|
||||
|
||||
// Search for facts
|
||||
try {
|
||||
const searchResult = await searchFacts(query)
|
||||
const facts = searchResult?.facts || []
|
||||
|
||||
// Update with search results
|
||||
root.render(
|
||||
createElement(InputSuggestionsPopup, {
|
||||
facts,
|
||||
isLoading: false,
|
||||
onFactSelect: handleFactSelect,
|
||||
onClose: handleClose
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error searching facts:", error)
|
||||
// Show error state
|
||||
root.render(
|
||||
createElement(InputSuggestionsPopup, {
|
||||
facts: [],
|
||||
isLoading: false,
|
||||
onFactSelect: handleFactSelect,
|
||||
onClose: handleClose
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Input field monitoring
|
||||
export function checkInputFields() {
|
||||
// Check for page navigation first
|
||||
checkPageNavigation()
|
||||
|
||||
const inputs = document.querySelectorAll(
|
||||
"input[type='text'], input[type='search'], input[type='email'], input[type='url'], textarea, [contenteditable='true']"
|
||||
)
|
||||
|
||||
inputs.forEach((input) => {
|
||||
const element = input as HTMLElement
|
||||
const inputElement = element as HTMLInputElement | HTMLTextAreaElement
|
||||
|
||||
// Skip Core extension popup textarea
|
||||
if (
|
||||
element.id === "memory-text" ||
|
||||
element.closest('[style*="z-index: 10002"]')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
let value = ""
|
||||
if (inputElement.value !== undefined) {
|
||||
value = inputElement.value.trim()
|
||||
} else {
|
||||
value = element.textContent?.trim() || ""
|
||||
}
|
||||
|
||||
const words = value.split(/\s+/).filter((word) => word.length > 0)
|
||||
|
||||
if (words.length > 1) {
|
||||
const rect = element.getBoundingClientRect()
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
createInputDot(element, rect)
|
||||
}
|
||||
} else {
|
||||
removeInputDot(element)
|
||||
removeInputPopup(element)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Cleanup functions
|
||||
function removeInputDot(input: HTMLElement) {
|
||||
const dot = inputDots.get(input)
|
||||
if (dot) {
|
||||
dot.remove()
|
||||
inputDots.delete(input)
|
||||
}
|
||||
}
|
||||
|
||||
export function removeInputPopup(input: HTMLElement) {
|
||||
// Clean up React root first
|
||||
const root = reactRoots.get(input)
|
||||
if (root) {
|
||||
root.unmount()
|
||||
reactRoots.delete(input)
|
||||
}
|
||||
|
||||
// Remove DOM element
|
||||
const popup = inputPopups.get(input)
|
||||
if (popup) {
|
||||
popup.remove()
|
||||
inputPopups.delete(input)
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAllInputElements() {
|
||||
// Clean up React roots
|
||||
reactRoots.forEach((root) => root.unmount())
|
||||
reactRoots.clear()
|
||||
|
||||
// Clean up DOM elements
|
||||
inputDots.forEach((dot) => dot.remove())
|
||||
inputPopups.forEach((popup) => popup.remove())
|
||||
inputDots.clear()
|
||||
inputPopups.clear()
|
||||
}
|
||||
|
||||
// Check if click is on input elements
|
||||
export function isInputElement(target: HTMLElement): boolean {
|
||||
// Check if target is within any popup or dot
|
||||
for (const popup of inputPopups.values()) {
|
||||
if (popup.contains(target)) return true
|
||||
}
|
||||
for (const dot of inputDots.values()) {
|
||||
if (dot.contains(target)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Handle input popup click outside
|
||||
export function handleInputClickOutside(target: HTMLElement) {
|
||||
inputPopups.forEach((popup, input) => {
|
||||
if (!popup.contains(target) && target !== input) {
|
||||
removeInputPopup(input)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Close all input popups
|
||||
export function closeAllInputPopups() {
|
||||
inputPopups.forEach((popup, input) => {
|
||||
removeInputPopup(input)
|
||||
})
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
import type { PlasmoCSConfig } from "plasmo"
|
||||
|
||||
import {
|
||||
checkInputFields,
|
||||
clearAllInputElements,
|
||||
closeAllInputPopups,
|
||||
handleInputClickOutside,
|
||||
isInputElement
|
||||
} from "~contents/input-suggestions"
|
||||
import {
|
||||
handleSelectionClickOutside,
|
||||
handleTextSelection,
|
||||
isSelectionElement,
|
||||
removeSelectionElements
|
||||
} from "~contents/text-selection"
|
||||
import { setupContextMonitoring } from "~utils/context"
|
||||
|
||||
export const config: PlasmoCSConfig = {
|
||||
matches: ["<all_urls>"]
|
||||
}
|
||||
|
||||
// Global cleanup function
|
||||
function globalCleanup() {
|
||||
console.log("Performing global cleanup...")
|
||||
removeSelectionElements()
|
||||
clearAllInputElements()
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
function initializeExtension() {
|
||||
try {
|
||||
console.log("Core extension initialized")
|
||||
|
||||
// Setup context monitoring with cleanup
|
||||
const stopMonitoring = setupContextMonitoring(globalCleanup)
|
||||
|
||||
// Text selection handler
|
||||
document.addEventListener("mouseup", handleTextSelection)
|
||||
|
||||
// Keyboard event handlers
|
||||
document.addEventListener("keyup", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
removeSelectionElements()
|
||||
closeAllInputPopups()
|
||||
}
|
||||
})
|
||||
|
||||
// Monitor input fields
|
||||
setInterval(checkInputFields, 1000)
|
||||
|
||||
// Initial check
|
||||
setTimeout(checkInputFields, 1000)
|
||||
|
||||
// Click outside to close popups
|
||||
document.addEventListener("click", (e) => {
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
// Don't close if clicking on extension elements
|
||||
if (isSelectionElement(target) || isInputElement(target)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle click outside for both features
|
||||
handleSelectionClickOutside(target)
|
||||
handleInputClickOutside(target)
|
||||
})
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener("beforeunload", () => {
|
||||
stopMonitoring()
|
||||
globalCleanup()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Extension initialization error:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initializeExtension)
|
||||
} else {
|
||||
initializeExtension()
|
||||
}
|
||||
@ -1,340 +0,0 @@
|
||||
import { addEpisode } from "~utils/api"
|
||||
import { isExtensionContextValid } from "~utils/context"
|
||||
import { createRoot, type Root } from "react-dom/client"
|
||||
import { createElement } from "react"
|
||||
import TextSelectionPopup from "~components/TextSelectionPopup"
|
||||
|
||||
let selectionDot: HTMLDivElement | null = null
|
||||
let selectionPopup: HTMLDivElement | null = null
|
||||
let selectionRoot: Root | null = null
|
||||
let autoHideTimer: NodeJS.Timeout | null = null
|
||||
let isHoveringElements = false
|
||||
|
||||
// Logo dot for text selection
|
||||
function createSelectionDot(rect: DOMRect) {
|
||||
removeSelectionElements()
|
||||
|
||||
selectionDot = document.createElement("div")
|
||||
selectionDot.style.cssText = `
|
||||
position: absolute;
|
||||
width: 54px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
z-index: 10001;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
|
||||
background: #c15e50;
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
// Add the logo SVG with white color and Core text
|
||||
selectionDot.innerHTML = `
|
||||
<div style="display: flex; flex-direction: row; align-items: center; gap: 5px;">
|
||||
<svg width="16" height="16" viewBox="0 0 282 282" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M80.0827 34.974C92.7457 3.8081 120.792 17.7546 134.476 29.5676C135.325 30.301 135.792 31.3761 135.792 32.4985V250.806C135.792 251.98 135.258 253.117 134.349 253.858C103.339 279.155 85.2835 259.158 80.0827 245.771C44.9187 241.11 43.965 209.932 47.8837 194.925C15.173 187.722 17.5591 152.731 22.841 136.135C9.34813 107.747 33.9141 90.4097 47.8837 85.2899C40.524 50.5456 66.2831 37.2692 80.0827 34.974Z" stroke="white" stroke-width="5"/>
|
||||
<circle cx="103.955" cy="66.3968" r="11.0444" fill="white"/>
|
||||
<circle cx="90.4996" cy="129.403" r="11.5465" fill="white"/>
|
||||
<circle cx="103.955" cy="197.449" r="11.0444" fill="white"/>
|
||||
<circle cx="48.4992" cy="86.0547" r="8.53434" fill="white"/>
|
||||
<path d="M202.806 247.026C190.143 278.192 162.097 264.245 148.413 252.432C147.563 251.699 147.097 250.624 147.097 249.501V31.1935C147.097 30.0203 147.631 28.8833 148.54 28.1417C179.549 2.84476 197.605 22.8418 202.806 36.2294C237.97 40.8903 238.924 72.0684 235.005 87.0748C267.716 94.2779 265.33 129.269 260.048 145.865C273.541 174.253 248.975 191.59 235.005 196.71C242.365 231.454 216.606 244.731 202.806 247.026Z" stroke="white" stroke-width="5"/>
|
||||
<circle cx="178.934" cy="215.603" r="11.0444" transform="rotate(180 178.934 215.603)" fill="white"/>
|
||||
<circle cx="192.389" cy="152.597" r="11.5465" transform="rotate(180 192.389 152.597)" fill="white"/>
|
||||
<circle cx="178.934" cy="84.5506" r="11.0444" transform="rotate(180 178.934 84.5506)" fill="white"/>
|
||||
<circle cx="234.389" cy="195.945" r="8.53434" transform="rotate(180 234.389 195.945)" fill="white"/>
|
||||
</svg>
|
||||
<div style="
|
||||
color: white;
|
||||
font-size: 8px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
">Core</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const top = rect.bottom + window.scrollY + 5
|
||||
const left = Math.min(
|
||||
rect.right + window.scrollX - 16,
|
||||
window.innerWidth - 40
|
||||
)
|
||||
|
||||
selectionDot.style.top = `${top}px`
|
||||
selectionDot.style.left = `${left}px`
|
||||
|
||||
selectionDot.addEventListener("click", showSelectionPopup)
|
||||
selectionDot.addEventListener("mouseenter", showSelectionPopup)
|
||||
|
||||
document.body.appendChild(selectionDot)
|
||||
|
||||
setTimeout(() => {
|
||||
if (selectionDot) {
|
||||
selectionDot.style.opacity = "1"
|
||||
}
|
||||
}, 10)
|
||||
|
||||
// Start auto-hide timer
|
||||
startAutoHideTimer(8000) // Longer initial timer
|
||||
|
||||
// Handle hover events
|
||||
selectionDot.addEventListener("mouseenter", () => {
|
||||
isHoveringElements = true
|
||||
clearAutoHideTimer()
|
||||
})
|
||||
|
||||
selectionDot.addEventListener("mouseleave", () => {
|
||||
isHoveringElements = false
|
||||
if (!selectionPopup) {
|
||||
startAutoHideTimer(3000) // Shorter timer when leaving dot
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Timer management functions
|
||||
function startAutoHideTimer(delay: number) {
|
||||
clearAutoHideTimer()
|
||||
autoHideTimer = setTimeout(() => {
|
||||
if (!isHoveringElements) {
|
||||
removeSelectionElements()
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
|
||||
function clearAutoHideTimer() {
|
||||
if (autoHideTimer) {
|
||||
clearTimeout(autoHideTimer)
|
||||
autoHideTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// Popup for text selection with React
|
||||
function showSelectionPopup() {
|
||||
const selectedText = window.getSelection()?.toString().trim()
|
||||
if (!selectedText || !selectionDot) return
|
||||
|
||||
removeSelectionPopup()
|
||||
|
||||
// Create popup container
|
||||
selectionPopup = document.createElement("div")
|
||||
selectionPopup.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 10002;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
`
|
||||
|
||||
// Position popup
|
||||
const dotRect = selectionDot.getBoundingClientRect()
|
||||
let top = dotRect.bottom + 5
|
||||
let left = dotRect.left
|
||||
|
||||
// Keep popup within viewport bounds
|
||||
if (left + 350 > window.innerWidth) {
|
||||
left = window.innerWidth - 360
|
||||
}
|
||||
if (left < 10) {
|
||||
left = 10
|
||||
}
|
||||
|
||||
// If popup would go below viewport, show above the dot
|
||||
if (top + 250 > window.innerHeight) {
|
||||
top = dotRect.top - 255
|
||||
}
|
||||
|
||||
// Ensure popup doesn't go above viewport
|
||||
if (top < 10) {
|
||||
top = 10
|
||||
}
|
||||
|
||||
selectionPopup.style.top = `${top}px`
|
||||
selectionPopup.style.left = `${left}px`
|
||||
|
||||
document.body.appendChild(selectionPopup)
|
||||
|
||||
// Handle popup hover events
|
||||
selectionPopup.addEventListener("mouseenter", () => {
|
||||
isHoveringElements = true
|
||||
clearAutoHideTimer()
|
||||
})
|
||||
|
||||
selectionPopup.addEventListener("mouseleave", () => {
|
||||
isHoveringElements = false
|
||||
startAutoHideTimer(5000) // Auto-hide after leaving popup
|
||||
})
|
||||
|
||||
// Prevent popup from closing when clicking inside it
|
||||
selectionPopup.addEventListener("click", (e) => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
|
||||
// Create React root
|
||||
selectionRoot = createRoot(selectionPopup)
|
||||
|
||||
// State management for the component
|
||||
let saveStatus: 'idle' | 'saving' | 'success' | 'error' | 'empty' = 'idle'
|
||||
let isSaving = false
|
||||
|
||||
// Handle save
|
||||
const handleSave = async (text: string) => {
|
||||
if (!isExtensionContextValid()) {
|
||||
saveStatus = 'error'
|
||||
renderPopup()
|
||||
return
|
||||
}
|
||||
|
||||
if (!text.trim()) {
|
||||
saveStatus = 'empty'
|
||||
renderPopup()
|
||||
setTimeout(() => {
|
||||
saveStatus = 'idle'
|
||||
renderPopup()
|
||||
}, 2000)
|
||||
return
|
||||
}
|
||||
|
||||
isSaving = true
|
||||
saveStatus = 'saving'
|
||||
renderPopup()
|
||||
|
||||
try {
|
||||
const success = await addEpisode(text)
|
||||
|
||||
if (success) {
|
||||
saveStatus = 'success'
|
||||
renderPopup()
|
||||
setTimeout(() => removeSelectionElements(), 1500)
|
||||
} else {
|
||||
saveStatus = 'error'
|
||||
renderPopup()
|
||||
setTimeout(() => {
|
||||
saveStatus = 'idle'
|
||||
isSaving = false
|
||||
renderPopup()
|
||||
}, 2000)
|
||||
}
|
||||
} catch (error) {
|
||||
saveStatus = 'error'
|
||||
renderPopup()
|
||||
setTimeout(() => {
|
||||
saveStatus = 'idle'
|
||||
isSaving = false
|
||||
renderPopup()
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle close
|
||||
const handleClose = () => {
|
||||
removeSelectionElements()
|
||||
}
|
||||
|
||||
// Render function
|
||||
const renderPopup = () => {
|
||||
if (selectionRoot) {
|
||||
selectionRoot.render(
|
||||
createElement(TextSelectionPopup, {
|
||||
selectedText,
|
||||
onSave: handleSave,
|
||||
onClose: handleClose,
|
||||
isSaving,
|
||||
saveStatus
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Initial render
|
||||
renderPopup()
|
||||
|
||||
// Show popup
|
||||
setTimeout(() => {
|
||||
if (selectionPopup) {
|
||||
selectionPopup.style.opacity = "1"
|
||||
}
|
||||
}, 10)
|
||||
}
|
||||
|
||||
// Text selection handler
|
||||
export function handleTextSelection() {
|
||||
const selection = window.getSelection()
|
||||
const selectedText = selection?.toString().trim()
|
||||
|
||||
if (
|
||||
selectedText &&
|
||||
selectedText.length > 0 &&
|
||||
selection &&
|
||||
selection.rangeCount > 0
|
||||
) {
|
||||
const range = selection.getRangeAt(0)
|
||||
const rect = range.getBoundingClientRect()
|
||||
createSelectionDot(rect)
|
||||
} else {
|
||||
removeSelectionElements()
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup functions
|
||||
export function removeSelectionElements() {
|
||||
clearAutoHideTimer()
|
||||
isHoveringElements = false
|
||||
|
||||
if (selectionDot) {
|
||||
selectionDot.remove()
|
||||
selectionDot = null
|
||||
}
|
||||
removeSelectionPopup()
|
||||
}
|
||||
|
||||
function removeSelectionPopup() {
|
||||
// Clean up React root first
|
||||
if (selectionRoot) {
|
||||
selectionRoot.unmount()
|
||||
selectionRoot = null
|
||||
}
|
||||
|
||||
// Remove DOM element
|
||||
if (selectionPopup) {
|
||||
selectionPopup.remove()
|
||||
selectionPopup = null
|
||||
}
|
||||
}
|
||||
|
||||
// Check if click is on selection elements
|
||||
export function isSelectionElement(target: HTMLElement): boolean {
|
||||
// Check if clicking on the dot
|
||||
if (selectionDot && selectionDot.contains(target)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if clicking on the popup (including textarea and buttons)
|
||||
if (selectionPopup && selectionPopup.contains(target)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if clicking on specific popup elements by ID
|
||||
if (target.id === 'memory-text' || target.id === 'save-btn' || target.id === 'close-btn') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if target is within any element with our popup z-index
|
||||
if (target.closest('[style*="z-index: 10002"]')) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Close selection popup if clicking elsewhere
|
||||
export function handleSelectionClickOutside(target: HTMLElement) {
|
||||
// Only close if we're not clicking on any selection elements
|
||||
if (selectionPopup && !isSelectionElement(target)) {
|
||||
removeSelectionElements()
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "core-extension",
|
||||
"displayName": "Core extension",
|
||||
"version": "0.0.1",
|
||||
"description": "Core memory extension ",
|
||||
"author": "Redplanethq (harshith@tegon.ai)",
|
||||
"scripts": {
|
||||
"dev": "plasmo dev",
|
||||
"build": "plasmo build",
|
||||
"package": "plasmo package"
|
||||
},
|
||||
"dependencies": {
|
||||
"plasmo": "0.90.5",
|
||||
"@plasmohq/storage": "1.15.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "4.1.1",
|
||||
"@types/chrome": "0.0.258",
|
||||
"@types/node": "20.11.5",
|
||||
"@types/react": "18.2.48",
|
||||
"@types/react-dom": "18.2.18",
|
||||
"prettier": "3.2.4",
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"manifest": {
|
||||
"host_permissions": [
|
||||
"https://*/*"
|
||||
],
|
||||
"permissions": [
|
||||
"storage"
|
||||
]
|
||||
}
|
||||
}
|
||||
6331
apps/extension/pnpm-lock.yaml
generated
6331
apps/extension/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,151 +0,0 @@
|
||||
import { useState } from "react"
|
||||
|
||||
import { useStorage } from "@plasmohq/storage/hook"
|
||||
|
||||
function IndexPopup() {
|
||||
// useStorage returns [value, setValue, { remove }]
|
||||
const [apiKey, setApiKey, { remove }] = useStorage<string>("core_api_key")
|
||||
const [inputValue, setInputValue] = useState(apiKey ?? "")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [message, setMessage] = useState("")
|
||||
|
||||
// Keep inputValue in sync with apiKey if apiKey changes externally
|
||||
// (e.g. from another tab)
|
||||
// This is optional, but helps UX
|
||||
if (apiKey !== undefined && inputValue !== apiKey) {
|
||||
setInputValue(apiKey)
|
||||
}
|
||||
|
||||
const saveApiKey = async () => {
|
||||
if (!inputValue.trim()) {
|
||||
setMessage("Please enter an API key")
|
||||
return
|
||||
}
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await setApiKey(inputValue.trim())
|
||||
setMessage("API key saved successfully!")
|
||||
setTimeout(() => setMessage(""), 3000)
|
||||
} catch (error) {
|
||||
setMessage("Failed to save API key")
|
||||
console.error("Save error:", error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const clearApiKey = async () => {
|
||||
try {
|
||||
await remove()
|
||||
setInputValue("")
|
||||
setMessage("API key cleared")
|
||||
setTimeout(() => setMessage(""), 3000)
|
||||
} catch (error) {
|
||||
setMessage("Failed to clear API key")
|
||||
console.error("Clear error:", error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20, minWidth: 300 }}>
|
||||
<h2 style={{ margin: "0 0 16px 0", fontSize: 18, color: "#333" }}>
|
||||
Core Memory Extension
|
||||
</h2>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label
|
||||
style={{
|
||||
display: "block",
|
||||
marginBottom: 8,
|
||||
fontSize: 14,
|
||||
color: "#555"
|
||||
}}>
|
||||
Core API Key:
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
placeholder="Enter your Core API key"
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "8px 12px",
|
||||
border: "1px solid #ddd",
|
||||
borderRadius: 4,
|
||||
fontSize: 14,
|
||||
marginBottom: 8
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
|
||||
<button
|
||||
onClick={saveApiKey}
|
||||
disabled={isLoading}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "8px 16px",
|
||||
background: "#c15e50",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: 4,
|
||||
cursor: isLoading ? "not-allowed" : "pointer",
|
||||
fontSize: 14,
|
||||
opacity: isLoading ? 0.6 : 1
|
||||
}}>
|
||||
{isLoading ? "Saving..." : "Save"}
|
||||
</button>
|
||||
|
||||
{apiKey && (
|
||||
<button
|
||||
onClick={clearApiKey}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
background: "#EF4444",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: 4,
|
||||
cursor: "pointer",
|
||||
fontSize: 14
|
||||
}}>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div
|
||||
style={{
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
fontSize: 13,
|
||||
backgroundColor: message.includes("success")
|
||||
? "#10B981"
|
||||
: "#EF4444",
|
||||
color: "white",
|
||||
marginBottom: 12
|
||||
}}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{apiKey && (
|
||||
<div style={{ fontSize: 12, color: "#10B981", marginBottom: 16 }}>
|
||||
✓ API key configured
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 13, color: "#666", lineHeight: 1.4 }}>
|
||||
<p style={{ margin: "0 0 8px 0" }}>
|
||||
<strong>How to use:</strong>
|
||||
</p>
|
||||
<ul style={{ margin: 0, paddingLeft: 16 }}>
|
||||
<li>Select text on any page to save to Core</li>
|
||||
<li>Look for red dots in input fields for AI suggestions</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default IndexPopup
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"extends": "plasmo/templates/tsconfig.base",
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"include": [
|
||||
".plasmo/index.d.ts",
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"~*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
import { Storage } from "@plasmohq/storage"
|
||||
|
||||
import { isExtensionContextValid, withValidContext } from "./context"
|
||||
|
||||
const storage = new Storage()
|
||||
const API_BASE_URL = "https://core.heysol.ai/api/v1"
|
||||
|
||||
export interface CoreEpisode {
|
||||
episodeBody: string
|
||||
referenceTime: Date
|
||||
source: string
|
||||
}
|
||||
|
||||
export interface CoreSearchQuery {
|
||||
query: string
|
||||
}
|
||||
|
||||
export interface CoreSearchResponse {
|
||||
facts: string[]
|
||||
episodes: CoreEpisode[]
|
||||
}
|
||||
|
||||
async function getApiKey(): Promise<string | null> {
|
||||
if (!isExtensionContextValid()) {
|
||||
console.log("Extension context invalid, cannot get API key")
|
||||
return null
|
||||
}
|
||||
|
||||
return withValidContext(async () => {
|
||||
return await storage.get("core_api_key")
|
||||
}, null)
|
||||
}
|
||||
|
||||
export async function addEpisode(episodeBody: string): Promise<boolean> {
|
||||
const apiKey = await getApiKey()
|
||||
if (!apiKey) {
|
||||
console.error("No API key found")
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/add`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
episodeBody,
|
||||
referenceTime: new Date(),
|
||||
source: "Core extension"
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("Failed to add episode:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchFacts(
|
||||
query: string
|
||||
): Promise<CoreSearchResponse | null> {
|
||||
const apiKey = await getApiKey()
|
||||
if (!apiKey) {
|
||||
console.error("No API key found")
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/search`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify({ query })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const responseJson = await response.json()
|
||||
|
||||
return responseJson
|
||||
} catch (error) {
|
||||
console.error("Failed to search facts:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
// Utility to check if extension context is still valid
|
||||
export function isExtensionContextValid(): boolean {
|
||||
try {
|
||||
// Try to access chrome.runtime - this will throw if context is invalidated
|
||||
return !!chrome?.runtime?.id
|
||||
} catch (error) {
|
||||
console.log("Extension context invalidated")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for operations that require valid extension context
|
||||
export async function withValidContext<T>(
|
||||
operation: () => Promise<T>,
|
||||
fallback?: T
|
||||
): Promise<T | undefined> {
|
||||
if (!isExtensionContextValid()) {
|
||||
console.log("Skipping operation - extension context invalid")
|
||||
return fallback
|
||||
}
|
||||
|
||||
try {
|
||||
return await operation()
|
||||
} catch (error) {
|
||||
if (error.message?.includes("Extension context invalidated")) {
|
||||
console.log("Extension context invalidated during operation")
|
||||
return fallback
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Graceful cleanup when context becomes invalid
|
||||
export function setupContextMonitoring(cleanupCallback: () => void) {
|
||||
// Check context periodically
|
||||
const checkInterval = setInterval(() => {
|
||||
if (!isExtensionContextValid()) {
|
||||
console.log("Extension context lost, cleaning up...")
|
||||
cleanupCallback()
|
||||
clearInterval(checkInterval)
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
// Listen for disconnect events
|
||||
try {
|
||||
chrome.runtime?.onConnect?.addListener((port) => {
|
||||
port.onDisconnect.addListener(() => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.log("Port disconnected, cleaning up...")
|
||||
cleanupCallback()
|
||||
}
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
// Ignore if chrome.runtime is not available
|
||||
}
|
||||
|
||||
return () => clearInterval(checkInterval)
|
||||
}
|
||||
@ -42,6 +42,7 @@ const data = {
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const user = useUser();
|
||||
console.log(user);
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
|
||||
@ -12,8 +12,9 @@ import { SidebarMenu, SidebarMenuItem, useSidebar } from "../ui/sidebar";
|
||||
import type { User } from "~/models/user.server";
|
||||
import { Button } from "../ui";
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import { ExtendedUser } from "~/hooks/useUser";
|
||||
|
||||
export function NavUser({ user }: { user: User }) {
|
||||
export function NavUser({ user }: { user: ExtendedUser }) {
|
||||
const { isMobile } = useSidebar();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -38,10 +39,15 @@ export function NavUser({ user }: { user: User }) {
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate font-medium">
|
||||
Harshith Mullapudi
|
||||
</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{user.email}
|
||||
</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
Credits: {user.availableCredits}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
@ -4,6 +4,10 @@ import { type loader } from "~/root";
|
||||
import { useChanged } from "./useChanged";
|
||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
export interface ExtendedUser extends User {
|
||||
availableCredits?: number;
|
||||
}
|
||||
|
||||
export function useIsImpersonating(matches?: UIMatch[]) {
|
||||
const data = useTypedMatchesData({
|
||||
id: "routes/_app.workspace.$workspaceSlug",
|
||||
@ -12,16 +16,18 @@ export function useIsImpersonating(matches?: UIMatch[]) {
|
||||
return data?.isImpersonating === true;
|
||||
}
|
||||
|
||||
export function useOptionalUser(matches?: UIMatch[]): User | undefined {
|
||||
export function useOptionalUser(matches?: UIMatch[]): ExtendedUser | undefined {
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: "root",
|
||||
matches,
|
||||
});
|
||||
|
||||
return routeMatch?.user ?? undefined;
|
||||
return routeMatch?.user
|
||||
? { ...routeMatch?.user, availableCredits: routeMatch?.availableCredits }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function useUser(matches?: UIMatch[]): User {
|
||||
export function useUser(matches?: UIMatch[]): ExtendedUser {
|
||||
const maybeUser = useOptionalUser(matches);
|
||||
if (!maybeUser) {
|
||||
throw new Error(
|
||||
@ -31,7 +37,9 @@ export function useUser(matches?: UIMatch[]): User {
|
||||
return maybeUser;
|
||||
}
|
||||
|
||||
export function useUserChanged(callback: (user: User | undefined) => void) {
|
||||
export function useUserChanged(
|
||||
callback: (user: ExtendedUser | undefined) => void,
|
||||
) {
|
||||
useChanged(useOptionalUser, callback);
|
||||
}
|
||||
|
||||
|
||||
@ -177,6 +177,18 @@ export async function getUserById(id: User["id"]) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getUserLeftCredits(id: User["id"]) {
|
||||
const userUsage = await prisma.userUsage.findFirst({ where: { userId: id } });
|
||||
|
||||
if (!userUsage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...userUsage,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getUserByEmail(email: User["email"]) {
|
||||
return prisma.user.findUnique({ where: { email } });
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ import {
|
||||
type ToastMessage,
|
||||
} from "./models/message.server";
|
||||
import { env } from "./env.server";
|
||||
import { getUser } from "./services/session.server";
|
||||
import { getUser, getUserRemainingCount } from "./services/session.server";
|
||||
import { usePostHog } from "./hooks/usePostHog";
|
||||
import {
|
||||
AppContainer,
|
||||
@ -49,10 +49,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const { getTheme } = await themeSessionResolver(request);
|
||||
|
||||
const posthogProjectKey = env.POSTHOG_PROJECT_KEY;
|
||||
const user = await getUser(request);
|
||||
const usage = await getUserRemainingCount(request);
|
||||
|
||||
return typedjson(
|
||||
{
|
||||
user: await getUser(request),
|
||||
user: user,
|
||||
availableCredits: usage?.availableCredits ?? 0,
|
||||
toastMessage,
|
||||
theme: getTheme(),
|
||||
posthogProjectKey,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { redirect } from "@remix-run/node";
|
||||
import { getUserById } from "~/models/user.server";
|
||||
import { getUserById, getUserLeftCredits } from "~/models/user.server";
|
||||
import { sessionStorage } from "./sessionStorage.server";
|
||||
import { getImpersonationId } from "./impersonation.server";
|
||||
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||
@ -25,6 +25,14 @@ export async function getUser(request: Request) {
|
||||
throw await logout(request);
|
||||
}
|
||||
|
||||
export async function getUserRemainingCount(request: Request) {
|
||||
const userId = await getUserId(request);
|
||||
if (userId === undefined) return null;
|
||||
|
||||
const userUsage = await getUserLeftCredits(userId);
|
||||
if (userUsage) return userUsage;
|
||||
}
|
||||
|
||||
export async function requireUserId(request: Request, redirectTo?: string) {
|
||||
const userId = await getUserId(request);
|
||||
if (!userId) {
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "dotenv -- turbo run build",
|
||||
"dev": "dotenv -- turbo run dev --filter=!core-extension --filter=!@redplanethq/core",
|
||||
"dev": "dotenv -- turbo run dev --filter=!@redplanethq/core",
|
||||
"lint": "dotenv -- turbo run lint",
|
||||
"format": "dotenv -- prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"check-types": "dotenv -- turbo run check-types",
|
||||
|
||||
4084
pnpm-lock.yaml
generated
4084
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user