Untitled

 avatar
unknown
plain_text
9 months ago
21 kB
10
Indexable

/**
 * 1) Convert an Arkham-style snippet {"component": "foo", ...} into an object.
 */
function parseArkhamComponent(segment: string): Record<string, string | number | null> {
    // 1) Strip outer { } braces
    const trimmed = segment.trim();
    if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
        throw new Error("Segment not wrapped in { ... }");
    }
    const inside = trimmed.slice(1, -1).trim();

    // Try parsing as JSON first
    try {
        const jsonObj = JSON.parse(trimmed);
        if (jsonObj.component) {
            return jsonObj;
        }
    } catch (e) {
        // Not valid JSON, continue with custom format parsing
    }

    // 2) Split fields by comma
    const fields = inside.split(",").map((p) => p.trim());
    const obj: Record<string, string | number | null> = {};

    // 3) For each field, split by the first colon
    fields.forEach((field) => {
        const [rawKey, ...rest] = field.split(":");
        if (!rawKey || !rest.length) {
            throw new Error("Invalid field: " + field);
        }
        const key = rawKey.trim();
        let val: string | number | null = rest.join(":").trim();

        // If it's wrapped in quotes, remove them
        if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
            val = val.slice(1, -1);
        }
        // If it's purely numeric, parse as number
        else if (/^-?\d+(\.\d+)?$/.test(val)) {
            val = parseFloat(val);
        }
        // Otherwise, treat as a string
        else {
            val = val.trim();
        }

        obj[key] = val;
    });

    return obj;
}

/**
 * 2) Break the big text string into {"component": ... } blocks + raw text,
 *    parse them, and return an array of React Nodes.
 */
export function ParseAiString(text: string, chain?: string, color = cssVar("gray")) {
    // First, try to unescape any escaped JSON strings in the text
    text = text.replace(/\\"/g, '"');

    const newlineRegex = /\n+/g; // Match one or more newlines
    const nodes: React.ReactNode[] = [];

    // Parse components by finding {"component": ...} with proper brace counting for nested objects
    const parts: (string | Record<string, any>)[] = [];
    let lastIndex = 0;

    for (let i = 0; i < text.length; i++) {
        // Look for start of component: {"component"
        if (text.substring(i, i + 1) === "{" && (text.substring(i + 1, i + 12) === '"component"' || text.substring(i + 1, i + 11) === "component:")) {
            // Add text before this component
            if (i > lastIndex) {
                parts.push(text.slice(lastIndex, i));
            }

            // Count braces to find the complete JSON object
            let braceCount = 0;
            let jsonEnd = i;
            for (let j = i; j < text.length; j++) {
                if (text[j] === "{") braceCount++;
                if (text[j] === "}") braceCount--;
                if (braceCount === 0) {
                    jsonEnd = j + 1;
                    break;
                }
            }

            const jsonString = text.slice(i, jsonEnd);
            try {
                const parsed = JSON.parse(jsonString);
                parts.push(parsed);
                lastIndex = jsonEnd;
                i = jsonEnd - 1;
            } catch (e) {
                // If parsing fails, treat as regular text
                parts.push(jsonString);
                lastIndex = jsonEnd;
                i = jsonEnd - 1;
            }
        }
    }

    // Add remaining text
    if (lastIndex < text.length) {
        parts.push(text.slice(lastIndex));
    }

    for (let i = 0; i < parts.length; i++) {
        const segment = parts[i];

        // Handle plain text segments
        if (typeof segment === "string") {
            if (segment) {
                // Split plain text by newlines and add spacing for deliberate newlines
                const lines = segment.split(newlineRegex);
                lines.forEach((line, index) => {
                    if (line) {
                        // Check if line starts with "* " for bullet point
                        const isBulletPoint = line.trimStart().startsWith("* ");
                        const bulletContent = isBulletPoint ? line.trimStart().substring(2) : line;
                        const contentToRender = isBulletPoint ? bulletContent : line;

                        // Replace "⇌" with the swap icon
                        const swapReplaced = contentToRender.split("⇌").map((part, j) =>
                            j > 0 ? (
                                <span key={`swap-${i}-${j}`}>
                                    {/* put on the baseline */}
                                    <div className={styles.llmSwapIcon}>
                                        <IoIosSwap />
                                    </div>
                                    {part}
                                </span>
                            ) : (
                                part
                            )
                        );

                        if (isBulletPoint) {
                            nodes.push(
                                <div key={`text-${i}-${index}`} style={{ color: color, display: "flex", gap: "0.5rem", marginLeft: "1rem" }}>
                                    <span style={{ flexShrink: 0 }}>•</span>
                                    <span>{swapReplaced}</span>
                                </div>
                            );
                        } else {
                            nodes.push(
                                <span key={`text-${i}-${index}`} style={{ color: color }}>
                                    {swapReplaced}
                                </span>
                            );
                        }
                    }
                    // Add spacing between deliberate newlines
                    if (index < lines.length - 1) {
                        nodes.push(<div key={`newline-${i}-${index}`} style={{ height: "1rem" }} />);
                    }
                });
            }
            continue;
        }

        // Handle component objects (already parsed)
        const parsed = segment;

        try {
            switch (parsed.component) {
                case "address": {
                    const addressString = typeof parsed.address === "string" ? parsed.address : "";
                    nodes.push(
                        <span key={i} style={{ display: "inline-block", color: cssVar("blue"), lineHeight: "1em" }}>
                            <Address
                                address={{ address: addressString, chain: chain as Chain }}
                                shorthand={true}
                                ignoreChain={chain != "solana"}
                                baseline
                                addressTitle
                                llmTransactionAddress
                                copyToClipboard={false}
                            />
                        </span>
                    );
                    break;
                }

                case "amount": {
                    const tokenAmount = typeof parsed.tokenAmount === "number" ? parsed.tokenAmount : undefined;
                    const usdValue = typeof parsed.usdValue === "number" ? parsed.usdValue : undefined;

                    // Determine if we should show USD value:
                    // - If only USD value is specified, show it even if 0
                    // - If both tokenAmount and usdValue exist, only show USD if it's not 0
                    const shouldShowUsd = usdValue != null && (tokenAmount == null || usdValue != 0);

                    nodes.push(
                        <span key={i} className={styles.llmTransactionAmount}>
                            {tokenAmount != null && (
                                <span style={{ color: cssVar("off-white") }}>
                                    <Number value={tokenAmount} compact={true} />
                                </span>
                            )}
                            {tokenAmount != null && shouldShowUsd && " "}
                            {shouldShowUsd && (
                                <span>
                                    {tokenAmount != null ? (
                                        <span style={{ color: usdValue < 0 ? cssVar("red") : usdValue > 0 ? cssVar("green") : cssVar("gray") }}>
                                            (
                                            <Number
                                                showSign={usdValue < 0}
                                                colored={true}
                                                value={usdValue}
                                                usd
                                                compact={true}
                                                decimals={usdValue > 0.01 ? 2 : Math.max(2, -Math.floor(Math.log10(Math.abs(usdValue)))) + 2}
                                            />
                                            )
                                        </span>
                                    ) : (
                                        <>
                                            <Number
                                                showSign={usdValue < 0}
                                                colored={true}
                                                value={usdValue}
                                                compact={true}
                                                usd
                                                decimals={usdValue > 0.01 ? 2 : Math.max(2, -Math.floor(Math.log10(Math.abs(usdValue)))) + 2}
                                            />
                                        </>
                                    )}
                                </span>
                            )}
                        </span>
                    );
                    break;
                }

                case "token": {
                    const tokenSymbol = typeof parsed.tokenSymbol === "string" ? parsed.tokenSymbol : "";
                    const tokenId = typeof parsed.tokenId === "string" ? parsed.tokenId : undefined;
                    const address = typeof parsed.address === "string" ? parsed.address : undefined;
                    const href = tokenId && tokenId != "" ? `/explorer/token/${tokenId}` : `/explorer/token/${chain}/${address}`;
                    nodes.push(
                        <span key={i} style={{ display: "inline-flex" }}>
                            <Link className={styles.underline} href={href}>
                                <WithIcon type="token" id={tokenId} className={styles.llmTransactionToken} baseline={true}>
                                    {tokenSymbol.toUpperCase()}
                                </WithIcon>
                            </Link>
                        </span>
                    );
                    break;
                }

                case "instrument": {
                    const instrumentSymbol = typeof parsed.instrumentSymbol === "string" ? parsed.instrumentSymbol : "";
                    const baseTokenId = typeof parsed.baseTokenId === "string" ? parsed.baseTokenId : "";
                    const quoteTokenId = typeof parsed.quoteTokenId === "string" ? parsed.quoteTokenId : "";

                    const singleIcon = baseTokenId && (!quoteTokenId || quoteTokenId === "");
                    const href = `/explorer/token/${baseTokenId}`;

                    nodes.push(
                        <span key={i} style={{ display: "inline-flex", alignItems: "center" }}>
                            {/* token icons stacked/overlapped */}
                            {!singleIcon ? (
                                <>
                                    <span className={styles.instrumentIcons /* see CSS stub below */}>
                                        <WithIcon type="token" id={baseTokenId} className={styles.instrumentIconBase} baseline />
                                        <WithIcon type="token" id={quoteTokenId} className={styles.instrumentIconQuote} baseline />
                                    </span>
                                    <Link className={styles.underline} href={href}>
                                        <span className={styles.instrumentSymbol /* optional style */}>{instrumentSymbol.toUpperCase()}</span>
                                    </Link>
                                </>
                            ) : (
                                <Link className={styles.underline} href={href}>
                                    <WithIcon type="token" id={baseTokenId} className={styles.instrumentIconSingle} baseline={true}>
                                        {instrumentSymbol.toUpperCase()}
                                    </WithIcon>
                                </Link>
                            )}
                        </span>
                    );
                    break;
                }

                case "instrument": {
                    console.log("Parsing instrument component:", parsed);
                    const instrumentSymbol = typeof parsed.instrumentSymbol === "string" ? parsed.instrumentSymbol : "";
                    const baseTokenId = typeof parsed.baseTokenId === "string" ? parsed.baseTokenId : "";
                    const quoteTokenId = typeof parsed.quoteTokenId === "string" ? parsed.quoteTokenId : "";

                    const singleIcon = baseTokenId && (!quoteTokenId || quoteTokenId === "");

                    console.log("Parsed instrument:", parsed, baseTokenId, quoteTokenId, singleIcon);

                    const href = `/explorer/token/${baseTokenId}`;
                    nodes.push(
                        <>
                            {!singleIcon ? (
                                <span key={i} style={{ display: "inline-flex", alignItems: "center" }}>
                                    {/* token icons */}
                                    <Link className={styles.underline} href={href}>
                                        <span className={styles.instrumentIcons}>
                                            <WithIcon type="token" id={baseTokenId} className={styles.instrumentIconBase} baseline />
                                            <WithIcon type="token" id={quoteTokenId} className={styles.instrumentIconQuote} baseline />
                                        </span>
                                    </Link>
                                    <span className={styles.instrumentSymbol}>{instrumentSymbol.toUpperCase()}</span>
                                </span>
                            ) : (
                                <span key={i} style={{ display: "inline-flex" }}>
                                    <Link className={styles.underline} href={href}>
                                        <WithIcon type="token" id={baseTokenId} className={styles.llmTransactionToken} baseline={true}>
                                            {instrumentSymbol.toUpperCase()}
                                        </WithIcon>
                                    </Link>
                                </span>
                            )}
                        </>
                    );
                    break;
                }

                case "entity": {
                    const entityId = typeof parsed.entityId === "string" ? parsed.entityId : undefined;
                    const entityName = typeof parsed.entityName === "string" ? parsed.entityName : "";

                    nodes.push(
                        <span key={i} style={{ display: "inline-flex" }}>
                            <Link className={styles.underline} href={`/explorer/entity/${entityId}`}>
                                <WithIcon type="entity" id={entityId} className={styles.llmTransactionToken} baseline={true}>
                                    {entityName}
                                </WithIcon>
                            </Link>
                        </span>
                    );
                    break;
                }

                case "dashboard-unit": {
                    const config = parsed.config;
                    if (config && typeof config === "object") {
                        // Render dashboard unit inline
                        const unitConfig = config as any; // Unit config from parsed JSON
                        nodes.push(
                            <div key={i} style={{ marginTop: "1rem", marginBottom: "1rem", maxHeight: "40em", overflow: "auto" }}>
                                <DashboardUnitComponent unit={{ ...unitConfig, id: "", width: 4, height: 4, top: 0, left: 0 }} updateUnit={() => {}} />
                            </div>
                        );
                    }
                    break;
                }

                case "instruction": {
                    const instruction = typeof parsed.instruction === "string" ? parsed.instruction : "";
                    nodes.push(
                        <span key={i} style={{ display: "inline-block" }}>
                            <div className={styles.solanaLlmInstruction}>{instruction}</div>
                        </span>
                    );
                    break;
                }

                case "tx": {
                    const txHash = typeof parsed.txHash === "string" ? parsed.txHash : "";
                    const order = typeof parsed.role === "number" ? parsed.role : 1;
                    const analyzedTxOrder = typeof parsed.isTarget === "number" ? parsed.isTarget : 0;

                    // Determine label and color based on `order`
                    let label = "";
                    let boxColorClass = ""; // CSS class controlling the box color

                    if (order === 1) {
                        label = "FRONTRUN:";
                        boxColorClass = styles.redBox; // e.g. background-color: var(--red);
                    } else if (order === 2) {
                        label = "VICTIM:";
                        boxColorClass = styles.greenBox; // e.g. background-color: var(--green);
                    } else {
                        label = "BACKRUN:";
                        boxColorClass = styles.redBox; // e.g. background-color: var(--red);
                    }

                    // Shorten the transaction hash
                    const shortHash = txHash.slice(0, 5);

                    // Add spacing before the component
                    nodes.push(<br key={`linebreak-before-${i}`} />);
                    nodes.push(<br key={`linebreak-before-${i}-2`} />);

                    // Push the styled component to the `nodes` array
                    nodes.push(
                        <span key={i} className={`$ ${boxColorClass}`}>
                            {label}
                            <span style={{ color: cssVar("gray"), fontWeight: "normal" }}>{" In "}</span>
                            {analyzedTxOrder ? (
                                // Render "(This TX)" if the order matches the analyzed tx order
                                <span className={styles.thisTx}>this TX</span>
                            ) : (
                                <>
                                    <span className={styles.thisTx}>TX:</span>

                                    <Link href={`/explorer/tx/${txHash}`} className={styles.txLink} target="_blank" rel="noopener noreferrer">
                                        <>{shortHash}</>
                                    </Link>
                                </>
                            )}
                            <span className={styles.thisTx}>,</span>
                        </span>
                    );

                    break;
                }

                case "txHash": {
                    const txHash = typeof parsed.txHash === "string" ? parsed.txHash : "";
                    const datetime = typeof parsed.datetime === "string" ? parsed.datetime : "";

                    // Render the txHash component with the same styling as the time column in transfer panel
                    nodes.push(
                        <Link key={i} href={`/explorer/tx/${txHash}`} className={styles.txHashLink} target="_blank" rel="noopener noreferrer">
                            {datetime}
                        </Link>
                    );

                    break;
                }

                default:
                    // Unknown component type - render as JSON string
                    nodes.push(<span key={i}>{JSON.stringify(parsed)}</span>);
                    break;
            }
        } catch (err) {
            console.error("Failed to render component:", parsed, err);
            nodes.push(<span key={i}>{JSON.stringify(parsed)}</span>);
        }
    }

    return nodes;
}
Editor is loading...
Leave a Comment