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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/app/components/app/columns/column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { COLUMNS, useColumnStore } from "@/state/columns"
import { usePlugStore } from "@/state/plugs"

import { SparklingText } from "../utils/sparkling-text"
import { ColumnChat } from "./utils/column-chat"

const MIN_COLUMN_WIDTH = 420
const MAX_COLUMN_WIDTH = 680
Expand Down Expand Up @@ -247,6 +248,8 @@ export const ConsoleColumn: FC<{
<div className="flex-1 overflow-y-auto rounded-b-lg">
{column.key === COLUMNS.KEYS.ADD ? (
<ColumnAdd index={column.index} />
) : column.key === COLUMNS.KEYS.CHAT ? (
<ColumnChat index={column.index} />
) : column.key === COLUMNS.KEYS.DISCOVER ? (
<PlugsDiscover index={column.index} className="pt-4" />
) : column.key === COLUMNS.KEYS.MY_PLUGS ? (
Expand Down
11 changes: 8 additions & 3 deletions packages/app/components/app/columns/utils/column-add.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Link from "next/link"
import { useMemo } from "react"

import { Activity, Cable, Cog, Coins, ExternalLink, Globe, ImageIcon, PiggyBank, Plug, Plus, Star, LockIcon } from "lucide-react"
import { Activity, Cable, Cog, Coins, Globe, ImageIcon, PiggyBank, Plug, Plus, Star, LockIcon, MessageCircle } from "lucide-react"

import { Accordion } from "@/components/shared/utils/accordion"
import { cn, formatTitle } from "@/lib"
Expand All @@ -26,6 +25,11 @@ export const ANONYMOUS_OPTIONS: Options = [
label: "MY_PLUGS",
description: "Create, edit, and run your Plugs.",
icon: <Cable size={14} className="opacity-40" />
},
{
label: "CHAT",
description: "Chat with Biblo to help build your Plugs.",
icon: <MessageCircle size={14} className="opacity-40" />
}
]

Expand Down Expand Up @@ -68,7 +72,8 @@ export const ColumnAdd = ({ index }: { index: number }) => {
description: "Install Plug as an app on your device.",
icon: <Star size={14} className="opacity-40" />
})



if (socket?.admin) {
options.push({
label: "ADMIN",
Expand Down
179 changes: 179 additions & 0 deletions packages/app/components/app/columns/utils/column-chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { useEffect, useState } from "react"
import Markdown from "react-markdown"

import { SearchIcon } from "lucide-react"

import { Button } from "@/components/shared/buttons/button"
import { Counter } from "@/components/shared/utils/counter"
import { cn, formatTitle } from "@/lib"
import { api } from "@/server/client"

import { Search } from "../../inputs/search"

interface Message {
message: string
isSent: boolean
}

const TypingIndicator = () => {
return (
<div className="flex justify-start">
<div className="flex items-center gap-1 rounded-lg bg-plug-green/10 p-3">
<div className="h-2 w-2 animate-bounce rounded-full bg-black/60" style={{ animationDelay: "0ms" }} />
<div className="h-2 w-2 animate-bounce rounded-full bg-black/60" style={{ animationDelay: "150ms" }} />
<div className="h-2 w-2 animate-bounce rounded-full bg-black/60" style={{ animationDelay: "300ms" }} />
</div>
</div>
)
}

const DEFAULT_TEXT = "👋 Hey, I'm Morgan. I can answer nearly any question about anything you see here in Plug.\n\nMy knowledge may be limited."
const DEFAULT_MESSAGE = { message: DEFAULT_TEXT, isSent: false }

const QUICK_MESSAGES = [
"What can I do with the tokens I hold?",
"What is the best place to earn yield?",
"What can I do with Plug?"
]

export const ColumnChat = ({ }: { index: number }) => {
const [messages, setMessages] = useState<Message[]>([DEFAULT_MESSAGE])
const [message, setMessage] = useState("")

const [isTyping, setIsTyping] = useState(false)
const [activeTools, setActiveTools] = useState<string[]>([])

const chat = api.biblo.chat.message.useMutation()

const handleSubmit = async (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()

if (!message.trim()) return

setMessages(prev => [...prev, { message: message, isSent: true }])
setIsTyping(true)
setMessage("")

try {
const response = await chat.mutateAsync({
message: message,
history: messages.slice(-20).map(msg => ({
content: msg.message,
role: msg.isSent ? "user" : "assistant"
}))
})

if (response.tools?.length) {
setActiveTools(prev => [...new Set([...prev, ...response.tools])])
}

const fullResponse = [response.reply, ...response.additionalMessages].join("\n\n")

setMessages(prev => [...prev, { message: fullResponse, isSent: false }])
} catch (error: any) {
const errorMessage = error.shape?.message || error.message || "Unknown error occurred"
const errorCode = error.shape?.code || error.code

setMessages(prev => [
...prev,
{
message: `Error Code ${errorCode}: ${errorMessage}`,
isSent: false
}
])
} finally {
setIsTyping(false)
}
}

return (
<>
<div className="relative flex h-full flex-col gap-2 p-4">
<div className="absolute left-0 right-0 top-0 h-24 bg-gradient-to-b from-plug-white to-transparent" />

<div className="messages flex h-full flex-col-reverse gap-2 overflow-y-auto pb-2 pt-24">
{isTyping && <TypingIndicator />}
{[...messages].reverse().map((msg, index) => (
<div key={index} className={`flex ${msg.isSent ? "justify-end" : "justify-start"} px-2`}>
<div
className={cn(
"max-w-[100%] rounded-lg p-4 font-bold",
msg.isSent ? "bg-plug-yellow text-plug-green" : "bg-plug-green/10 text-black/60"
)}
>
<Markdown
components={{
ul: ({ children }) => (
<ul className="ml-8 mr-4 list-disc text-justify">{children}</ul>
),
ol: ({ children }) => (
<ol className="ml-8 mr-4 list-decimal text-justify">{children}</ol>
),
li: ({ children }) => <li className="list-item">{children}</li>,
p: ({ children }) => <p className="">{children}</p>
}}
>
{msg.message}
</Markdown>
</div>
</div>
))}
</div>

<div className="flex h-max flex-col gap-2 bg-plug-white pt-2">
{activeTools.length > 0 && (
<div className="group flex w-full flex-row gap-1 overflow-x-scroll">
<Button
className="ml-auto flex flex-row gap-2 group-hover:hidden"
variant="secondary"
onClick={() => { }}
sizing="sm"
>
<Counter count={activeTools.length} /> Tools Used
</Button>
<div className="hidden flex-row gap-2 group-hover:flex">
{activeTools.map((tool, index) => (
<Button
key={index}
className="w-max"
variant="secondary"
sizing="sm"
onClick={() => { }}
>
{formatTitle(tool)}
</Button>
))}
</div>
</div>
)}

<div className="flex flex-row gap-2 overflow-x-scroll">
{QUICK_MESSAGES.map((msg, index) => (
<Button
key={index}
className="w-max"
variant="secondary"
onClick={handleSubmit}
sizing="sm"
>
<span className="opacity-60">&quot;</span>
{msg}
<span className="opacity-60">&quot;</span>
</Button>
))}
</div>

<form onSubmit={handleSubmit} className="flex flex-col gap-2">
<Search
icon={<SearchIcon size={16} />}
search={message}
handleSearch={message => setMessage(message)}
placeholder="Type a message..."
/>
<Button className="w-full py-4" onClick={handleSubmit}>Send</Button>
</form>
</div>
</div>
</>
)
}
36 changes: 28 additions & 8 deletions packages/app/lib/functions/plug/solver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@ import { ActionSchemas } from "@/lib/types"

let cachedSchemas: Record<string, ActionSchemas | undefined> = {}

export const schemas = async (protocol?: string, action?: string, chainId: number = 8453, from?: string): Promise<ActionSchemas> => {
export const schemas = async (
protocol?: string,
action?: string,
chainId: number = 8453,
from?: string
): Promise<ActionSchemas> => {
const cacheKey = from ? `${protocol}-${action}-${from}` : `${protocol}-${action}`

if (cachedSchemas[cacheKey]) return cachedSchemas[cacheKey]
console.log("[Schemas] Request:", { protocol, action, chainId, from })

if (cachedSchemas[cacheKey]) {
console.log("[Schemas] Cache hit:", cacheKey)
return cachedSchemas[cacheKey]
}

const response = await axios.get(`${env.SOLVER_URL}/solver`, {
params: {
Expand All @@ -20,10 +30,12 @@ export const schemas = async (protocol?: string, action?: string, chainId: numbe
chainId
},
headers: {
'X-Api-Key': env.SOLVER_API_KEY
"X-Api-Key": env.SOLVER_API_KEY
}
})

console.log("[Schemas] Response:", response.data)

if (response.status !== 200) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" })

cachedSchemas[cacheKey] = response.data as ActionSchemas
Expand All @@ -40,8 +52,12 @@ export const intent = async (input: {
[key: string]: string | number
}>
}) => {
console.log("[Intent] Request:", input)

const response = await axios.post(`${env.SOLVER_URL}/solver`, input)

console.log("[Intent] Response:", response.data)

if (response.status !== 200) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" })

return response.data
Expand All @@ -50,7 +66,7 @@ export const intent = async (input: {
export const killed = async () => {
const response = await axios.get(`${env.SOLVER_URL}/solver/kill`, {
headers: {
'X-Api-Key': env.SOLVER_API_KEY
"X-Api-Key": env.SOLVER_API_KEY
}
})

Expand All @@ -60,11 +76,15 @@ export const killed = async () => {
}

export const kill = async () => {
const response = await axios.post(`${env.SOLVER_URL}/solver/kill`, {}, {
headers: {
'X-Api-Key': env.SOLVER_API_KEY
const response = await axios.post(
`${env.SOLVER_URL}/solver/kill`,
{},
{
headers: {
"X-Api-Key": env.SOLVER_API_KEY
}
}
})
)

if (response.status !== 200) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" })

Expand Down
8 changes: 7 additions & 1 deletion packages/app/lib/functions/zerion/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const prohibitedSymbolInclusions = [...prohibitedNameInclusions, "claim", "airdr

const SECOND = 1000
// const MINUTE = 60 * second
const POSITIONS_CACHE_TIME = 60 * SECOND
const POSITIONS_CACHE_TIME = 60 * SECOND * 30

const getZerionPositions = async (chains: string[], socketId: string, socketAddress?: string) => {
const response = await axios.get(
Expand Down Expand Up @@ -406,6 +406,12 @@ const findPositions = async (id: string, search: string = "") => {
* @throws {TRPCError} Throws a FORBIDDEN error if the socket address isn't the address of the wallet owned socket.
*/
export const getPositions = async (address: string, socketAddress?: string, search?: string, chains = ["base"]) => {
console.log("Zerion getPositions:", {
address,
socketAddress,
chains,
timestamp: new Date().toISOString()
})
const socket = await db.userSocket.findFirst({
where: { id: address }
})
Expand Down
Loading