Skip to content

Add UI for dynamic code block to execute Assistant wp-cli commands #214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3b4b651
Update Assistant Markdown styles to use class instead of id
derekblank Jun 5, 2024
1c77bad
Remove unneccessary container div
derekblank Jun 5, 2024
a3eba8e
Merge branch 'fix/markdown-updates' into feat/dynamic-assistant-code-…
derekblank Jun 6, 2024
5f146df
Add UI for Assistant dynamic code blocks
derekblank Jun 6, 2024
5964848
Update todo notes
derekblank Jun 6, 2024
178842f
Remove inline from from Markdown render
derekblank Jun 6, 2024
d6baaeb
Remove duplicate pre tag when parsing code blocks
derekblank Jun 6, 2024
ef34a80
Update styling for non-Markdown code blocks
derekblank Jun 6, 2024
981a20e
Update Copy Text button handler
derekblank Jun 6, 2024
2b67bda
Update ActionButton handlers to use a second label when actioned
derekblank Jun 6, 2024
8c26996
Add secondIcon to ActionButton
derekblank Jun 6, 2024
5ac1502
Update Run button handler
derekblank Jun 6, 2024
c6dfe6a
Update ActionButton styles to use tertiary variant
derekblank Jun 6, 2024
80d195d
Add a Spinner component to InlineCLI block
derekblank Jun 6, 2024
12289a0
Update isRunning state
derekblank Jun 6, 2024
d36263b
Merge trunk and resolve conflicts
derekblank Jun 6, 2024
7ab76fc
Use natural margins for Assistant Markdown headings
derekblank Jun 7, 2024
ce1b83e
Refactor markdown code components to CodeBlock
derekblank Jun 7, 2024
19202ac
Fix Markdown code block typing
derekblank Jun 10, 2024
b821649
Merge remote-tracking branch 'origin' into feat/dynamic-assistant-cod…
derekblank Jun 11, 2024
bac4d2a
Fix unauthenticated view HTML syntax
derekblank Jun 11, 2024
a0bcac3
Disable Run button for code block for now
Jun 11, 2024
65313ac
Replace spinner with wordpress components
sejas Jun 11, 2024
6e48014
Translate error and success
Jun 11, 2024
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
14 changes: 14 additions & 0 deletions src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ const secondaryStyles = `
[&.is-secondary:not(:focus)]:aria-disabled:hover:shadow-a8c-gray-5
`.replace( /\n/g, ' ' );

const tertiaryStyles = `
[&.is-tertiary]:text-white
[&.is-tertiary]:bg-gray-700
[&.is-tertiary]:focus:bg-gray-600
[&.is-tertiary]:focus:text-white
[&.is-tertiary:not(.is-destructive,:disabled,[aria-disabled=true])]:hover:bg-gray-600
[&.is-tertiary:not(.is-destructive,:disabled,[aria-disabled=true])]:hover:text-white
[&.is-tertiary]:hover:bg-white
[&.is-tertiary]:hover:text-white
[&.is-tertiary]:disabled:bg-gray-700
[&.is-tertiary]:disabled:text-a8c-gray-50
`.replace( /\n/g, ' ' );

const destructiveStyles = `
[&.is-destructive:not(.is-primary)]:text-a8c-red-50
[&.is-destructive:not(.is-primary)]:hover:text-a8c-red-70
Expand Down Expand Up @@ -107,6 +120,7 @@ export default function ButtonComponent( {
baseStyles,
variant === 'primary' && primaryStyles,
variant === 'secondary' && secondaryStyles,
variant === 'tertiary' && tertiaryStyles,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leverage tertiary for InlineCLI ActionButtons. (tertiary is currently unused as a <Button /> variant in trunk.)

Copy link
Member

@dcalhoun dcalhoun Jun 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noted these new tertiary styles modified the existing usage of tertiary in the add site modal. We might consider consolidating button styles (i.e., ask the design team if we can reduce the number of button styles used) or rename this new style. WDYT?

Before After
before after

<Button onClick={ closeModal } disabled={ isAddingSite } variant="tertiary">

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dcalhoun Good catch! I think we should use a different name for the buttons in code blocks as they're a bit of a special use case. I don't anticipate on re-using those dark styles anywhere else in the app at the moment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, let's update these buttons to use the same styling as the Add site button in the sidebar (outlined, not filled). That helps bring more consistency to the button styles in the app. (Figma)

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For posterity, I drafted #243 addressing the button styling.

variant === 'link' && linkStyles,
variant === 'icon' && iconStyles,
props.isDestructive && destructiveStyles,
Expand Down
176 changes: 153 additions & 23 deletions src/components/content-tab-assistant.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Spinner } from '@wordpress/components';
import { createInterpolateElement } from '@wordpress/element';
import { Icon, external } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { Icon, external, copy } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import React, { useState, useEffect, useRef } from 'react';
import Markdown from 'react-markdown';
import Markdown, { ExtraProps } from 'react-markdown';
import { useAssistant } from '../hooks/use-assistant';
import { useAssistantApi } from '../hooks/use-assistant-api';
import { useAuth } from '../hooks/use-auth';
Expand All @@ -12,6 +14,7 @@ import { getIpcApi } from '../lib/get-ipc-api';
import { AIInput } from './ai-input';
import { MessageThinking } from './assistant-thinking';
import Button from './button';
import { ExecuteIcon } from './icons/execute';

interface ContentTabAssistantProps {
selectedSite: SiteDetails;
Expand All @@ -23,25 +26,150 @@ interface MessageProps {
className?: string;
}

export const Message = ( { children, isUser, className }: MessageProps ) => (
<div className={ cx( 'flex mt-4', isUser ? 'justify-end' : 'justify-start', className ) }>
<div
className={ cx(
'inline-block p-3 rounded-sm border border-gray-300 lg:max-w-[70%] select-text whitespace-pre-wrap',
! isUser && 'bg-white'
) }
>
{ typeof children === 'string' ? (
<div className="assistant-markdown">
<Markdown>{ children }</Markdown>
</div>
) : (
children
) }
interface InlineCLIProps {
output: string;
status: 'success' | 'error';
time: string;
}
Comment on lines +29 to +33
Copy link
Contributor Author

@derekblank derekblank Jun 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interface is likely to change based on the final shape of the wp-cli implementation in #203 -- just chose some probable params for UI demonstration purposes.


const InlineCLI = ( { output, status, time }: InlineCLIProps ) => (
<div className="p-3 bg-[#2D3337]">
<div className="flex justify-between mb-2 font-sans">
<span className={ status === 'success' ? 'text-[#63CE68]' : 'text-[#E66D6C]' }>
{ status === 'success' ? __( 'Success' ) : __( 'Error' ) }
</span>
<span className="text-gray-400">{ time }</span>
</div>
<pre className="text-white !bg-transparent !m-0 !px-0">
<code className="!bg-transparent !mx-0 !px-0">{ output }</code>
</pre>
</div>
);

const ActionButton = ( {
primaryLabel,
secondaryLabel,
icon,
onClick,
timeout,
disabled,
}: {
primaryLabel: string;
secondaryLabel: string;
icon: JSX.Element;
onClick: () => void;
timeout?: number;
disabled?: boolean;
} ) => {
const [ buttonLabel, setButtonLabel ] = useState( primaryLabel );

const handleClick = () => {
onClick();
setButtonLabel( secondaryLabel );
if ( timeout ) {
setTimeout( () => {
setButtonLabel( primaryLabel );
}, timeout );
}
};

return (
<Button
onClick={ handleClick }
variant="tertiary"
className="mr-2 font-sans select-none"
disabled={ disabled }
>
{ icon }
<span className="ml-1">{ buttonLabel }</span>
</Button>
);
};

export const Message = ( { children, isUser, className }: MessageProps ) => {
const [ cliOutput, setCliOutput ] = useState< string | null >( null );
const [ cliStatus, setCliStatus ] = useState< 'success' | 'error' | null >( null );
const [ cliTime, setCliTime ] = useState< string | null >( null );
const [ isRunning, setIsRunning ] = useState( false );

const handleExecute = () => {
setIsRunning( true );
setTimeout( () => {
setCliOutput(
`Installing Jetpack...\nUnpacking the package...\nInstalling the plugin...\nPlugin installed successfully.\nActivating 'jetpack'...\nPlugin 'jetpack' activated.\nSuccess: Installed 1 of 1 plugins.`
);
setCliStatus( 'success' );
setCliTime( 'Completed in 2.3 seconds' );
setIsRunning( false );
}, 2300 );
};

const CodeBlock = ( props: JSX.IntrinsicElements[ 'code' ] & ExtraProps ) => {
const { children, className } = props;
const match = /language-(\w+)/.exec( className || '' );
const content = String( children ).trim();

return match ? (
<>
<div className="p-3">
<code className={ className } { ...props }>
{ children }
</code>
</div>
<div className="p-3 mt-1 flex justify-start items-center">
<ActionButton
primaryLabel={ __( 'Copy' ) }
secondaryLabel={ __( 'Copied' ) }
icon={ <Icon icon={ copy } size={ 16 } /> }
onClick={ () => getIpcApi().copyText( content ) }
timeout={ 2000 }
/>
{ /* <ActionButton
primaryLabel={ __( 'Run' ) }
secondaryLabel={ __( 'Run Again' ) }
icon={ <ExecuteIcon /> }
onClick={ handleExecute }
disabled={ isRunning } */ }
</div>
{ isRunning && (
<div className="p-3 flex justify-start items-center bg-[#2D3337] text-white">
<Spinner className="!text-white [&>circle]:stroke-a8c-gray-60" />
<span className="ml-2 font-sans">{ __( 'Running...' ) }</span>
</div>
) }
{ ! isRunning && cliOutput && cliStatus && cliTime && (
<InlineCLI output={ cliOutput } status={ cliStatus } time={ cliTime } />
) }
</>
) : (
<div className="p-3">
<code className={ className } { ...props }>
{ children }
</code>
</div>
);
};

return (
<div className={ cx( 'flex mt-4', isUser ? 'justify-end' : 'justify-start', className ) }>
<div
className={ cx(
'inline-block p-3 rounded-sm border border-gray-300 lg:max-w-[70%] select-text whitespace-pre-wrap',
! isUser && 'bg-white'
) }
>
{ typeof children === 'string' ? (
<div className="assistant-markdown">
<Markdown components={ { code: CodeBlock } }>{ children }</Markdown>
</div>
) : (
children
) }
</div>
</div>
);
};

export function ContentTabAssistant( { selectedSite }: ContentTabAssistantProps ) {
const { messages, addMessage, clearMessages } = useAssistant( selectedSite.name );
const { fetchAssistant, isLoading: isAssistantThinking } = useAssistantApi();
Expand Down Expand Up @@ -115,9 +243,11 @@ export function ContentTabAssistant( { selectedSite }: ContentTabAssistantProps

const renderUnauthenticatedView = () => (
<Message className="w-full" isUser={ false }>
<p className="mb-1.5 a8c-label-semibold">{ __( 'Hold up!' ) }</p>
<p>{ __( 'You need to log in to your WordPress.com account to use the assistant.' ) }</p>
<p className="mb-1.5">
<div className="mb-3 a8c-label-semibold">{ __( 'Hold up!' ) }</div>
<div className="mb-1">
{ __( 'You need to log in to your WordPress.com account to use the assistant.' ) }
</div>
<div className="mb-1">
{ createInterpolateElement(
__( "If you don't have an account yet, <a>create one for free</a>." ),
{
Expand All @@ -133,10 +263,10 @@ export function ContentTabAssistant( { selectedSite }: ContentTabAssistantProps
),
}
) }
</p>
<p className="mb-3">
</div>
<div className="mb-3">
{ __( 'Every account gets 200 prompts included for free each month.' ) }
</p>
</div>
<Button variant="primary" onClick={ authenticate }>
{ __( 'Log in to WordPress.com' ) }
<Icon className="ltr:ml-1 rtl:mr-1 rtl:scale-x-[-1]" icon={ external } size={ 21 } />
Expand Down
8 changes: 8 additions & 0 deletions src/components/icons/execute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const ExecuteIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M2.6667 11.8535H13.3334V12.8535H2.6667V11.8535ZM2.64648 5.5404L4.62626 7.52018L2.64648 9.49996L3.35359 10.2071L5.68692 7.87374L6.04048 7.52018L5.68692 7.16663L3.35359 4.83329L2.64648 5.5404Z"
fill="currentColor"
/>
</svg>
);
15 changes: 1 addition & 14 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,8 @@ blockquote {
.assistant-markdown pre {
background-color: #1D2327;
border-radius: 5px;
margin: 1rem 0;
margin: 0.5rem 0;
overflow-x: auto;
padding: 0.5em;
}

.assistant-markdown pre code {
Expand All @@ -124,43 +123,31 @@ blockquote {
.assistant-markdown h1 {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
margin-top: 1.5rem;
}

.assistant-markdown h2 {
font-size: 1.25rem;
font-weight: bold;
margin-bottom: 0.5rem;
margin-top: 1.5rem;
}

.assistant-markdown h3 {
font-size: 1.125rem;
font-weight: bold;
margin-bottom: 0.5rem;
margin-top: 1.5rem;
}

.assistant-markdown h4 {
font-size: 1rem;
font-weight: bold;
margin-bottom: 0.5rem;
margin-top: 1.5rem;
}

.assistant-markdown h5 {
font-size: 0.875rem;
font-weight: bold;
margin-bottom: 0.5rem;
margin-top: 1.5rem;
}

.assistant-markdown h6 {
font-size: 0.85rem;
font-weight: bold;
margin-bottom: 0.5rem;
margin-top: 1.5rem;
}

.assistant-markdown hr {
Expand Down
Loading