Skip to content

Commit

Permalink
feat: Improve menu actions (#126)
Browse files Browse the repository at this point in the history
* feat: add custom actions

* wip

* fix: action button settings

* feat: draggable action button

* feat: implement logic in action buttons

* fix: shortcut

* more fixes

* more fixes

* more fixes

* fix: settings store

* fix: eslint

* fixes

* fix ui hydration
  • Loading branch information
paulclindo authored Feb 5, 2024
1 parent dc68824 commit fcc5fe3
Show file tree
Hide file tree
Showing 25 changed files with 870 additions and 225 deletions.
206 changes: 206 additions & 0 deletions apps/shinkai-visor/src/components/action-button/action-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import {
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { restrictToWindowEdges } from '@dnd-kit/modifiers';
import {
DraggableItem,
HoverCard,
HoverCardContent,
HoverCardTrigger,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@shinkai_network/shinkai-ui';
import { cn } from '@shinkai_network/shinkai-ui/utils';
import { motion } from 'framer-motion';
import { FileInputIcon, ScissorsIcon } from 'lucide-react';
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import { IntlProvider } from 'react-intl';

import shinkaiLogo from '../../assets/icons/shinkai-min.svg';
import { delay } from '../../helpers/misc';
import { srcUrlResolver } from '../../helpers/src-url-resolver';
import { useGlobalActionButtonChromeMessage } from '../../hooks/use-global-action-button-chrome-message';
import { langMessages, locale } from '../../lang/intl';
import { OPEN_SIDEPANEL_DELAY_MS } from '../../service-worker/action';
import { ServiceWorkerInternalMessageType } from '../../service-worker/communication/internal/types';
import { useSettings } from '../../store/settings/settings';
import themeStyle from '../../theme/styles.css?inline';
export const SHINKAI_ACTION_ELEMENT_NAME = 'shinkai-action-button-root';

const baseContainer = document.createElement(SHINKAI_ACTION_ELEMENT_NAME);
const shadow = baseContainer.attachShadow({ mode: 'open' });
const container = document.createElement('div');
container.id = 'root';
shadow.appendChild(container);
const htmlRoot = document.getElementsByTagName('html')[0];
htmlRoot.prepend(baseContainer);

const toggleSidePanel = async () => {
const isOpen = await chrome.runtime.sendMessage({
type: ServiceWorkerInternalMessageType.IsSidePanelOpen,
});
if (isOpen) {
await chrome.runtime.sendMessage({
type: ServiceWorkerInternalMessageType.CloseSidePanel,
});
} else {
await chrome.runtime.sendMessage({
type: ServiceWorkerInternalMessageType.OpenSidePanel,
});
}
};

const sendPage = async () => {
await chrome.runtime.sendMessage({
type: ServiceWorkerInternalMessageType.OpenSidePanel,
});
await delay(OPEN_SIDEPANEL_DELAY_MS);
await chrome.runtime.sendMessage({
type: ServiceWorkerInternalMessageType.SendPageToAgent,
});
};

const sendCapture = async () => {
await chrome.runtime.sendMessage({
type: ServiceWorkerInternalMessageType.OpenSidePanel,
});
await chrome.runtime.sendMessage({
type: ServiceWorkerInternalMessageType.SendCaptureToAgent,
});
};

const ActionButton = () => {
const displayActionButton = useSettings(
(settingsStore) => settingsStore.displayActionButton,
);
const sideButtonOffset = useSettings(
(settingsStore) => settingsStore.sideButtonOffset,
);
const setSideButtonOffset = useSettings(
(settingsStore) => settingsStore.setSideButtonOffset,
);
useGlobalActionButtonChromeMessage();

const activationConstraint = {
delay: 150,
tolerance: 5,
};

const mouseSensor = useSensor(MouseSensor, {
activationConstraint,
});
const touchSensor = useSensor(TouchSensor, {
activationConstraint,
});
const keyboardSensor = useSensor(KeyboardSensor, {});
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);

const isLeft = Math.abs(sideButtonOffset.x) >= window.innerWidth / 2;
return (
<DndContext
modifiers={[restrictToWindowEdges]}
onDragEnd={({ delta }) => {
setSideButtonOffset(({ x, y }) => ({
x: x + delta.x,
y: y + delta.y,
}));
}}
sensors={sensors}
>
<div
className={cn(
'z-max fixed transition-transform duration-300 ease-in-out',
displayActionButton
? 'translate-z-0 right-1 top-1'
: isLeft // adding extra 10% to hide the button
? '-translate-x-[110%]'
: 'translate-x-[110%]',
isLeft ? 'left-1 right-auto' : 'left-auto right-1',
)}
>
<DraggableItem top={sideButtonOffset.y}>
<HoverCard openDelay={150}>
<HoverCardTrigger asChild>
<motion.button
className="hover:bg-brand overflow shadow-4xl h-[48px] w-[48px] rounded-2xl bg-gray-500 p-2 shadow-2xl transition-colors duration-75 "
data-testid="action-button"
onClick={toggleSidePanel}
>
<img
alt="shinkai-app-logo select-none"
className={'h-full w-full select-none group-hover:rotate-45'}
src={srcUrlResolver(shinkaiLogo)}
/>
</motion.button>
</HoverCardTrigger>
<HoverCardContent>
<kbd className="pointer-events-none flex w-full select-none items-center rounded-md bg-gray-800 pl-2 font-mono text-sm font-medium text-gray-500 text-white">
<span></span>
<span>
<svg
className={'h-3 w-3'}
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
>
<path
d="M14.1995 12.343C13.4747 12.8201 12.6144 13.0486 11.7484 12.994C10.8824 12.9395 10.0576 12.6047 9.39845 12.0404C8.73931 11.476 8.28158 10.7126 8.09429 9.86531C7.907 9.01803 8.00031 8.13282 8.36015 7.34321C8.71999 6.5536 9.32684 5.90242 10.0892 5.48789C10.8515 5.07336 11.7279 4.91797 12.5863 5.04516C13.4447 5.17235 14.2384 5.57521 14.8478 6.19299C15.4571 6.81076 15.8491 7.60994 15.9645 8.46997C16.3295 10.262 16.4715 12.417 15.7575 14.368C14.9915 16.458 13.2935 18.172 10.2525 18.968C9.99906 19.0262 9.73293 18.9834 9.51052 18.8487C9.28811 18.7141 9.1269 18.498 9.06109 18.2465C8.99527 17.9949 9.03003 17.7276 9.15798 17.5013C9.28593 17.2749 9.49701 17.1073 9.74647 17.034C12.2065 16.39 13.3565 15.104 13.8795 13.68C14.0345 13.255 14.1395 12.806 14.1995 12.342"
fill="currentColor"
/>
</svg>
</span>
</kbd>
{[
{
label: 'Send Capture',
onClick: sendCapture,
icon: <ScissorsIcon className="h-full w-full" />,
},
{
label: 'Send Page',
onClick: sendPage,
icon: <FileInputIcon className="h-full w-full" />,
},
].map((item) => (
<TooltipProvider key={item.label}>
<Tooltip>
<TooltipTrigger asChild>
<button
className="hover:bg-brand flex h-8 w-8 items-center justify-center rounded-full bg-gray-500 p-2 text-white shadow-2xl transition-colors duration-75"
onClick={item.onClick}
>
{item.icon}
</button>
</TooltipTrigger>
<TooltipContent align="center" side="left" sideOffset={3}>
<p className="font-inter">{item.label}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
))}
</HoverCardContent>
</HoverCard>
</DraggableItem>
</div>
</DndContext>
);
};

const root = createRoot(container);
root.render(
<React.StrictMode>
<style>{themeStyle}</style>
<IntlProvider locale={locale} messages={langMessages}>
<ActionButton />
</IntlProvider>
</React.StrictMode>,
);
11 changes: 6 additions & 5 deletions apps/shinkai-visor/src/components/create-job/create-job.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export const CreateJob = () => {
const location = useLocation<{ files: File[]; agentName: string }>();
const query = useQuery();
const auth = useAuth((state) => state.auth);
const settings = useSettings((state) => state.settings);
// const settings = useSettings((state) => state.settings);
const currentDefaultAgentId = useSettings((state) => state.defaultAgentId);
const form = useForm<FormSchemaType>({
resolver: zodResolver(formSchema),
defaultValues: {
Expand Down Expand Up @@ -118,13 +119,13 @@ export const CreateJob = () => {
let defaultAgentId = '';
defaultAgentId =
defaultAgentId ||
(settings?.defaultAgentId &&
agents.find((agent) => agent.id === settings.defaultAgentId)
? settings.defaultAgentId
(currentDefaultAgentId &&
agents.find((agent) => agent.id === currentDefaultAgentId)
? currentDefaultAgentId
: '');
defaultAgentId = defaultAgentId || (agents?.length ? agents[0].id : '');
form.setValue('agent', defaultAgentId);
}, [form, location, agents, settings]);
}, [form, location, agents, currentDefaultAgentId]);
const submit = (values: FormSchemaType) => {
if (!auth) return;
let content = values.content;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ root.render(
<style>{themeStyle}</style>
<style>{reactCropStyle}</style>
<IntlProvider locale={locale} messages={langMessages}>
<div className="pointer-events-none fixed z-[2000000000] h-full w-full overflow-hidden">
<div className="z-max pointer-events-none fixed h-full w-full overflow-hidden">
<ImageCapture />
</div>
</IntlProvider>
Expand Down
49 changes: 34 additions & 15 deletions apps/shinkai-visor/src/components/settings/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useAgents } from '@shinkai_network/shinkai-node-state/lib/queries/getAg
import { useGetHealth } from '@shinkai_network/shinkai-node-state/lib/queries/getHealth/useGetHealth';
import {
Button,
Checkbox,
ExportIcon,
Form,
FormControl,
Expand All @@ -18,6 +17,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
Switch,
TextField,
} from '@shinkai_network/shinkai-ui';
import { useEffect } from 'react';
Expand All @@ -32,7 +32,7 @@ import { Header } from '../header/header';

const formSchema = z.object({
defaultAgentId: z.string(),
hideActionButton: z.boolean(),
displayActionButton: z.boolean(),
nodeAddress: z.string(),
shinkaiIdentity: z.string(),
nodeVersion: z.string(),
Expand All @@ -43,17 +43,27 @@ type FormSchemaType = z.infer<typeof formSchema>;
export const Settings = () => {
const history = useHistory();
const auth = useAuth((authStore) => authStore.auth);
const settings = useSettings((settingsStore) => settingsStore.settings);
const setSettings = useSettings((settingsStore) => settingsStore.setSettings);
const displayActionButton = useSettings(
(settingsStore) => settingsStore.displayActionButton,
);
const setDisplayActionButton = useSettings(
(settingsStore) => settingsStore.setDisplayActionButton,
);
const defaultAgentId = useSettings(
(settingsStore) => settingsStore.defaultAgentId,
);
const setDefaultAgentId = useSettings(
(settingsStore) => settingsStore.setDefaultAgentId,
);
const { nodeInfo, isSuccess: isNodeInfoSuccess } = useGetHealth({
node_address: auth?.node_address ?? '',
});
console.log(nodeInfo, 'nodeInfo');

const form = useForm<FormSchemaType>({
resolver: zodResolver(formSchema),
defaultValues: {
defaultAgentId: settings?.defaultAgentId,
hideActionButton: settings?.hideActionButton,
defaultAgentId: defaultAgentId,
displayActionButton: displayActionButton,
nodeAddress: auth?.node_address,
},
});
Expand Down Expand Up @@ -85,10 +95,18 @@ export const Settings = () => {
}, [form, isNodeInfoSuccess, nodeInfo?.node_name, nodeInfo?.version]);

useEffect(() => {
if (JSON.stringify(currentFormValue) !== JSON.stringify(settings)) {
setSettings({ ...currentFormValue });
}
}, [currentFormValue, settings, setSettings]);
setDisplayActionButton(
currentFormValue.displayActionButton ?? displayActionButton,
);
setDefaultAgentId(currentFormValue.defaultAgentId ?? defaultAgentId);
}, [
currentFormValue.displayActionButton,
currentFormValue.defaultAgentId,
setDisplayActionButton,
setDefaultAgentId,
displayActionButton,
defaultAgentId,
]);

return (
<div className="flex flex-col space-y-8 pr-2.5">
Expand Down Expand Up @@ -163,21 +181,22 @@ export const Settings = () => {
/>
<FormField
control={form.control}
name="hideActionButton"
name="displayActionButton"
render={({ field }) => (
<FormItem className="flex gap-2.5">
<FormControl id={'hide-action'}>
<Checkbox
<Switch
aria-readonly
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="static space-y-1.5 text-sm text-white">
<FormattedMessage id="hide-action-button-label" />
<FormattedMessage id="display-action-button-label" />
</FormLabel>
<FormDescription>
<FormattedMessage id="hide-action-button-description" />
<FormattedMessage id="show-action-button-description" />
</FormDescription>
</div>
</FormItem>
Expand Down
3 changes: 3 additions & 0 deletions apps/shinkai-visor/src/helpers/misc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function delay(e = 1e3) {
return new Promise((t) => setTimeout(t, e));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ServiceWorkerInternalMessageType } from '../service-worker/communication/internal/types';
import { useAuth } from '../store/auth/auth';
import { useSettings } from '../store/settings/settings';
import { useChromeMessage } from './use-chrome-message';

export const useGlobalActionButtonChromeMessage = () => {
useChromeMessage((message) => {
switch (message.type) {
case ServiceWorkerInternalMessageType.RehydrateStore:
useAuth.persist.rehydrate();
useSettings.persist.rehydrate();
break;
default:
break;
}
});
};
4 changes: 2 additions & 2 deletions apps/shinkai-visor/src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,6 @@
"identity-type": "Identity type",
"disconnect-warning-link": "Export your connection",
"disconnect-warning-link-description": "If you want to use it later",
"hide-action-button-label": "Hide action button",
"hide-action-button-description": "You will need to use ⌘ , hotkey to show/hide Shinkai Visor"
"display-action-button-label": "Display always sidebar icon",
"show-action-button-description": "You will need to use ⌘ , hotkey to show/hide Shinkai Visor. You can drag to move it up and down. "
}
4 changes: 2 additions & 2 deletions apps/shinkai-visor/src/lang/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,6 @@
"identity-type": "Tipo de identidad",
"disconnect-warning-link": "Exporta tu conexión",
"disconnect-warning-link-description": "Si quieres usarla más tarde",
"hide-action-button-label": "Ocultar botón de acción",
"hide-action-button-description": "Necesitarás usar el acceso rápido ⌘ , para mostar/ocultar Shinkai Visor"
"display-action-button-label": "Siempre mostrar botón de acción",
"show-action-button-description": "Necesitarás usar el acceso rápido ⌘ , para mostar/ocultar Shinkai Visor"
}
Loading

0 comments on commit fcc5fe3

Please sign in to comment.