[ { "role": "system", "content": "\n You are an Übersicht widget designer. Create Übersicht widgets when requested by the user.\n\n IMPORTANT: You have access to a tool called WriteUbersichtWidgetToFileSystem. When asked to create a widget, you MUST call this tool.\n\n ### Tool Usage:\n Call WriteUbersichtWidgetToFileSystem with complete JSX code that implements the Übersicht Widget API. Generate custom JSX based on the user's specific request - do not copy the example below.\n\n ### Übersicht Widget API (REQUIRED):\n Every Übersicht widget MUST export these 4 items:\n - export const command: The bash command to execute (string)\n - export const refreshFrequency: Refresh rate in milliseconds (number)\n - export const render: React component function that receives {output} prop (function)\n - export const className: CSS positioning for absolute placement (string)\n\n Example format (customize for each request):\n WriteUbersichtWidgetToFileSystem({jsxContent: `export const command = \"echo hello\"; export const refreshFrequency = 1000; export const render = ({output}) => { return
{output}
; }; export const className = \"top: 20px; left: 20px;\"`})\n\n ### Rules:\n - The terms \"ubersicht widget\", \"widget\", \"a widget\", \"the widget\" must all be interpreted as \"Übersicht widget\"\n - Generate complete, valid JSX code that follows the Übersicht widget API\n - When you generate a widget, don't just show JSON or code - you MUST call the WriteUbersichtWidgetToFileSystem tool\n - Report the results to the user after calling the tool\n\n ### Examples:\n - \"Generate a Übersicht widget\" → Use WriteUbersichtWidgetToFileSystem tool\n - \"Can you add a widget that shows the time\" → Use WriteUbersichtWidgetToFileSystem tool\n - \"Create a widget with a button\" → Use WriteUbersichtWidgetToFileSystem tool\n ", "tools": [ { "type": "function", "function": { "name": "WriteUbersichtWidgetToFileSystem", "description": "Writes an Übersicht Widget to the file system. Call this tool as the last step in processing a prompt that generates a widget.", "parameters": { "type": "object", "properties": { "jsxContent": { "type": "string", "description": "Complete JSX code for an Übersicht widget. This should include all required exports: command, refreshFrequency, render, and className. The JSX should be a complete, valid Übersicht widget file." } }, "required": [ "jsxContent" ] } } } ] }, { "role": "user", "content": "Create an Übersicht widget that elegantly displays the currently playing song from Apple Music or Spotify with album artwork and playback information. The widget should use React/JSX with a sophisticated music player interface featuring album covers, track details, progress bars, and smooth animations. Include multi-service support, adaptive colors from artwork, and customizable display options. Implement a premium design with proper music service integration and real-time updates." }, { "role": "assistant", "content": "I'll create that widget for you.", "tool_calls": [ { "id": "call_UeberPlayer", "type": "function", "function": { "name": "WriteUbersichtWidgetToFileSystem", "arguments": "{\"jsxContent\": \"// index.jsx\\nimport { styled, run } from \\\"uebersicht\\\";\\nimport getColors from './lib/getColors.js';\\nimport updateWidget from './lib/updater.js';\\nconst _version = '1.4.0';\\nlet _updateTimer = 8.5;\\n\\n/* CUSTOMIZATION (mess around here!)\\nYou may need to refresh the widget after changing these settings\\nFor more details about these settings: please visit https://github.com/aCluelessDanny/UeberPlayer#settings */\\n\\nconst options = {\\n /* Widget size! */\\n size: \\\"big\\\", // -> big (default) | medium | small | mini\\n\\n /* Widget position! */\\n verticalPosition: \\\"top\\\", // -> top (default) | center | bottom | \\\"\\\" | \\\"-\\\"\\n horizontalPosition: \\\"left\\\", // -> left (default) | center | right | \\\"\\\" | \\\"-\\\"\\n\\n /* Widget visibility! */\\n alwaysShow: 0, // -> 0 (default) | 1 | 2\\n\\n /* Adaptive colors! */\\n adaptiveColors: true, // -> true (default) | false\\n minContrast: 2.6, // -> 2.6 (default) | number\\n\\n /* Visual customization! */\\n dualProgressBar: false, // -> true | false (default)\\n miniArtwork: false, // -> true | false (default)\\n\\n /* Cache setting! */\\n cacheMaxDays: 15, // -> 15 (default) | \\n\\n /* Check for updates */\\n checkForUpdates: true // -> true (default) | false\\n}\\n\\n/* ROOT STYLING */\\n\\nexport const className = `\\n pointer-events: none;\\n position: absolute;\\n top: 0;\\n left: 0;\\n bottom: 0;\\n right: 0;\\n font-family: -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", Roboto, Helvetica, Arial, sans-serif, \\\"Apple Color Emoji\\\", \\\"Segoe UI Emoji\\\", \\\"Segoe UI Symbol\\\";\\n color: white;\\n\\n * {\\n box-sizing: border-box;\\n padding: 0;\\n border: 0;\\n margin: 0;\\n }\\n`;\\n\\n/* EMOTION COMPONENTS */\\n\\nconst wrapperPos = ({ horizontal, vertical }) => {\\n if (horizontal === \\\"center\\\" && vertical === \\\"center\\\") {\\n return `\\n top: 50%;\\n left: 50%;\\n transform: translate(-50%, -50%);\\n `\\n }\\n\\n let hPos, vPos;\\n switch (horizontal) {\\n case \\\"left\\\": hPos = `left: 20px;`; break;\\n case \\\"center\\\": hPos = `left: 50%; transform: translateX(-50%);`; break;\\n case \\\"right\\\": hPos = `right: 20px;`; break;\\n default: hPos = horizontal.startsWith(\\\"-\\\") ? `right: ${parseInt(horizontal) * -1}px;` : `left: ${horizontal}px;`; break;\\n }\\n switch (vertical) {\\n case \\\"top\\\": vPos = `top: 20px;`; break;\\n case \\\"center\\\": vPos = `top: 50%; transform: translateY(-50%);`; break;\\n case \\\"bottom\\\": vPos = `bottom: 20px;`; break;\\n default: vPos = vertical.startsWith(\\\"-\\\") ? `bottom: ${parseInt(vertical) * -1}px;` : `top: ${vertical}px;`; break;\\n }\\n\\n return `${hPos} ${vPos}`;\\n}\\n\\nconst Wrapper = styled(\\\"div\\\")`\\n position: absolute;\\n border-radius: 6px;\\n overflow: hidden;\\n box-shadow: 0 16px 32px 9px #0005;\\n opacity: ${props => props.show ? 1 : 0};\\n background: ${props => (props.bg !== undefined) ? props.bg : \\\"#0004\\\"};\\n transition: all 0.6s cubic-bezier(0.22, 1, 0.36, 1);\\n ${wrapperPos}\\n\\n &::before {\\n content: \\\"\\\";\\n position: absolute;\\n top: 0;\\n left: 0;\\n bottom: 0;\\n right: 0;\\n border-radius: 6px;\\n z-index: -1;\\n }\\n\\n * {\\n transition: all 0.6s cubic-bezier(0.22, 1, 0.36, 1);\\n }\\n`;\\n\\nconst miniWrapperPos = ({ horizontal }) => {\\n switch (horizontal) {\\n case \\\"left\\\": return `text-align: left;`;\\n case \\\"center\\\": return `text-align: center;`;\\n case \\\"right\\\": return `text-align: right;`;\\n default: return horizontal.startsWith(\\\"-\\\") ? `text-align: right;` : `text-align: left;`;\\n }\\n}\\n\\nconst MiniWrapper = styled(Wrapper)`\\n border-radius: 0;\\n overflow: visible;\\n box-shadow: none;\\n background: transparent;\\n ${miniWrapperPos}\\n\\n &::before {\\n display: none;\\n }\\n`\\n\\nconst ErrorWrapper = styled('div')`\\n width: 360px;\\n padding: 1em;\\n text-align: center;\\n font-size: .9em;\\n\\n * + * {\\n margin-top: .4em;\\n }\\n\\n h3 {\\n font-style: italic;\\n color: #ddd;\\n }\\n`\\n\\nconst BigPlayer = styled(\\\"div\\\")`\\n display: flex;\\n flex-direction: column;\\n width: 240px;\\n`;\\n\\nconst MediumPlayer = styled(BigPlayer)`\\n width: 180px;\\n`\\n\\nconst SmallPlayer = styled(\\\"div\\\")`\\n position: relative;\\n display: flex;\\n height: 80px;\\n width: 340px;\\n`\\n\\nconst MiniPlayer = styled(\\\"div\\\")`\\n position: relative;\\n display: flex;\\n flex-direction: column;\\n width: 400px;\\n line-height: 1;\\n\\n * {\\n text-shadow: 0px 0px 4px #0004, 0px 2px 12px #0004;\\n }\\n`\\n\\nconst MiniInfo = styled(\\\"div\\\")`\\n display: flex;\\n align-items: center;\\n`\\n\\nconst MiniDetails = styled(\\\"div\\\")`\\n flex: 1;\\n\\n > * + * {\\n margin-top: .5em;\\n }\\n`\\n\\nconst ArtworkWrapper = styled(\\\"div\\\")`\\n position: relative;\\n width: 240px;\\n height: 240px;\\n\\n &.medium {\\n width: 180px;\\n height: 180px;\\n }\\n\\n &.small {\\n width: 80px;\\n height: 80px;\\n }\\n\\n &.mini {\\n width: 65px;\\n height: 65px;\\n border-radius: 4px;\\n margin-right: 10px;\\n overflow: hidden;\\n }\\n\\n &::before {\\n position: absolute;\\n content: \\\"\\\";\\n top: 0;\\n left: 0;\\n bottom: 0;\\n right: 0;\\n border-radius: 6px 6px 0 0;\\n background: #fff1;\\n backdrop-filter: blur(8px) brightness(90%) contrast(80%) saturate(140%);\\n z-index: -1;\\n }\\n\\n &.mini::before {\\n border-radius: 0;\\n }\\n`\\n\\nconst Artwork = styled(\\\"img\\\")`\\n position: absolute;\\n top: 0;\\n left: 0;\\n bottom: 0;\\n right: 0;\\n height: 100%;\\n width: 100%;\\n object-fit: cover;\\n opacity: ${props => props.show ? 1 : 0};\\n transition: opacity .5s linear;\\n`;\\n\\nconst Information = styled(\\\"div\\\")`\\n position: relative;\\n padding: .5em .75em;\\n line-height: 1.3;\\n border-radius: 0 0 6px 6px;\\n backdrop-filter: ${options.adaptiveColors ? \\\"blur(8px)\\\" : \\\"blur(8px) brightness(90%) contrast(80%) saturate(140%)\\\"};\\n\\n > p {\\n text-align: center;\\n white-space: nowrap;\\n overflow: hidden;\\n text-overflow: ellipsis;\\n }\\n\\n &.small {\\n flex: 1;\\n width: 0;\\n display: flex;\\n flex-direction: column;\\n justify-content: center;\\n border-radius: 0 6px 6px 0;\\n line-height: 1.4;\\n }\\n\\n &.small > p {\\n text-align: left;\\n }\\n`\\n\\nconst Progress = styled(\\\"div\\\")`\\n position: absolute;\\n top: 0;\\n left: 0;\\n right: 0;\\n height: 2px;\\n background: ${props => options.dualProgressBar && props.emptyColor ? (props.emptyColor + \\\"80\\\") : \\\"transparent\\\"};\\n\\n &::after {\\n content: \\\"\\\";\\n position: absolute;\\n top: 0;\\n left: 0;\\n bottom: 0;\\n width: ${props => props.percent}%;\\n background: ${props => props.progressColor ? props.progressColor : \\\"white\\\"};\\n transition: width .6s cubic-bezier(0.22, 1, 0.36, 1);\\n }\\n\\n &.small {\\n top: initial;\\n bottom: 0;\\n }\\n\\n &.mini {\\n position: relative;\\n height: 4px;\\n border-radius: 2px;\\n background: ${props => options.dualProgressBar && props.emptyColor ? (props.emptyColor + \\\"60\\\") : \\\"#0005\\\"};\\n box-shadow: 0 3px 5px -1px #0003;\\n overflow: hidden;\\n margin-top: 10px;\\n }\\n`\\n\\nconst Track = styled(\\\"p\\\")`\\n font-weight: bold;\\n font-size: .7em;\\n color: ${props => props.color ? props.color : \\\"inherit\\\"};\\n\\n &::after {\\n content: '';\\n display: inline-block;\\n width: 0;\\n }\\n\\n &.small {\\n font-size: .65em;\\n }\\n\\n &.mini {\\n font-size: 1.2em;\\n }\\n`\\n\\nconst Artist = styled(\\\"p\\\")`\\n font-size: .7em;\\n color: ${props => props.color ? props.color : \\\"inherit\\\"};\\n\\n &::after {\\n content: '';\\n display: inline-block;\\n width: 0;\\n }\\n\\n &.small {\\n font-size: .65em;\\n }\\n\\n &.mini {\\n font-size: 1em;\\n }\\n`\\n\\nconst Album = styled(\\\"p\\\")`\\n font-size: .65em;\\n color: ${props => props.color ? props.color : \\\"inherit\\\"};\\n opacity: .75;\\n\\n &::after {\\n content: '';\\n display: inline-block;\\n width: 0;\\n }\\n\\n &.small {\\n font-size: .55em;\\n }\\n`\\n\\nconst UpdateNotifWrapper = styled(\\\"div\\\")`\\n position: absolute;\\n top: 0;\\n width: 100%;\\n padding: 1em;\\n min-height: 30%;\\n background-image: linear-gradient(to bottom, #000b, transparent);\\n`\\n\\nconst UpdateText = styled(\\\"p\\\")`\\n font-size: .7em;\\n text-align: center !important;\\n pointer-events: auto;\\n cursor: pointer;\\n\\n span {\\n color: inherit;\\n text-decoration: underline;\\n }\\n`\\n\\n/* UEBER-SPECIFIC STUFF */\\n\\nexport const init = (dispatch) => {\\n // Initialize and clear cache of old artwork\\n run(\\n `mkdir -p UeberPlayer.widget/cache &&\\\\\\n find UeberPlayer.widget/cache -mindepth 1 -type f -mtime +${options.cacheMaxDays} -delete &&\\\\\\n osascript UeberPlayer.widget/lib/init.scpt`\\n );\\n // Check for updates when enabled\\n if (options.checkForUpdates) {\\n checkForUpdate(dispatch);\\n setInterval(checkForUpdate, 86400000, dispatch);\\n }\\n};\\n\\nexport const command = \\\"osascript UeberPlayer.widget/lib/getTrack.scpt\\\";\\n\\nexport const initialState = {\\n app: \\\"\\\", // Current music software being used\\n playing: false, // If currently playing a soundtrack\\n appleMusicError: false, // If online music is being played on Apple Music\\n songChange: false, // If the song changed\\n primaryColor: undefined, // Primary color from artwork\\n secondaryColor: undefined, // Secondary color from artwork\\n tercaryColor: undefined, // Tercary color from artwork\\n artwork: { // Artwork source URL to be used\\n art1: \\\"UeberPlayer.widget/default.png\\\", // Artwork to alternate with\\n art2: \\\"UeberPlayer.widget/default.png\\\", // Same as above\\n alternate: true // Flag to pick which artwork to display (for smooth transitions)\\n },\\n song: { // Current song data\\n track: \\\"\\\", // Name of soundtrack\\n artist: \\\"\\\", // Name of artist\\n album: \\\"\\\", // Name of album\\n localArtwork: \\\"\\\", // Locally stored url for album artwork\\n onlineArtwork: \\\"\\\", // Online url for album artwork\\n duration: 0, // Total duration of soundtrack in seconds\\n elapsed: 0 // Total time elapsed in seconds\\n },\\n updateAvailable: false, // Flag for when an update's available\\n updatePending: false, // Flag for when an update's about to happen\\n};\\n\\n// Update state\\nexport const updateState = ({ type, output, error }, previousState) => {\\n switch (type) {\\n case 'UB/COMMAND_RAN':\\n return updateSongData(output, error, previousState);\\n case 'GET_ART':\\n if (options.adaptiveColors) {\\n return getColors(output, previousState, options);\\n } else {\\n const { art1, art2, alternate } = previousState.artwork;\\n return {\\n ...previousState,\\n songChange: false,\\n primaryColor: undefined,\\n secondaryColor: undefined,\\n tercaryColor: undefined,\\n artwork: {\\n art1: alternate ? art1 : output.img.src,\\n art2: !alternate ? art2 : output.img.src,\\n alternate: !alternate\\n }\\n }\\n }\\n case 'DEFAULT_ART':\\n const { art1, art2, alternate } = previousState.artwork;\\n return {\\n ...previousState,\\n songChange: false,\\n primaryColor: undefined,\\n secondaryColor: undefined,\\n tercaryColor: undefined,\\n artwork: {\\n art1: alternate ? art1 : \\\"UeberPlayer.widget/default.png\\\",\\n art2: !alternate ? art2 : \\\"UeberPlayer.widget/default.png\\\",\\n alternate: !alternate\\n }\\n }\\n case 'UPDATE_AVAILABLE':\\n return { ...previousState, updateAvailable: true }\\n case 'PREPARE_UPDATE':\\n console.log('Preparing update!');\\n const updateCountdown = setInterval(() => {\\n if (--_updateTimer <= 0) {\\n updateWidget(options);\\n window.clearInterval(updateCountdown);\\n }\\n }, 1000);\\n return { ...previousState, updatePending: true }\\n default:\\n console.error(\\\"Invalid dispatch type?\\\");\\n return previousState;\\n }\\n}\\n\\n/* FUNCTIONS */\\n\\n// Check for update\\nconst checkForUpdate = async (dispatch) => {\\n let resp = await fetch('https://raw.githubusercontent.com/aCluelessDanny/UeberPlayer/master/widget.json');\\n if (!resp.ok) { throw Error(\\\"Unable to check for update!\\\") }\\n\\n let data = await resp.json();\\n if (_version !== data.version) {\\n console.log(`There's an update available! -> ${data.version}`);\\n dispatch({ type: \\\"UPDATE_AVAILABLE\\\" });\\n }\\n}\\n\\n// Update song metadata\\nconst updateSongData = (output, error, previousState) => {\\n // Check for errors\\n if (error) {\\n const err_output = { ...previousState, error: error };\\n console.error(err_output);\\n return err_output;\\n }\\n\\n // Extract & parse applescript output\\n let [\\n playing,\\n app,\\n track,\\n artist,\\n album,\\n artworkURL,\\n artworkFilename,\\n duration,\\n elapsed,\\n appleMusicError\\n ] = output.trim().split(\\\" @@ \\\");\\n\\n playing = (playing === \\\"true\\\");\\n appleMusicError = (appleMusicError === \\\"true\\\");\\n duration = Math.floor(parseFloat(duration));\\n elapsed = Math.floor(parseFloat(elapsed));\\n\\n // State controller\\n if (!playing || appleMusicError) { // If player is paused or if online music is being played on Apple Music\\n return { ...previousState, app, playing, appleMusicError };\\n } else if (track !== previousState.song.track || album !== previousState.song.album) { // Song change\\n return {\\n ...previousState,\\n app,\\n playing,\\n appleMusicError,\\n songChange: true,\\n song: {\\n track,\\n artist,\\n album,\\n localArtwork: `UeberPlayer.widget/cache/${artworkFilename}`,\\n onlineArtwork: artworkURL,\\n duration,\\n elapsed\\n }\\n }\\n } else { // Currently playing\\n return {\\n ...previousState,\\n app,\\n playing,\\n appleMusicError,\\n song: {\\n ...previousState.song,\\n elapsed\\n }\\n };\\n }\\n}\\n\\n// Prepare artwork\\nconst prepareArtwork = (dispatch, song) => {\\n // Use a dummy image to test images beforehand\\n const img = new Image();\\n\\n // Attempts images in this order: Local -> Online -> Default\\n img.onload = () => { dispatch({ type: \\\"GET_ART\\\", output: { img }})};\\n img.onerror = () => {\\n if (song.onlineArtwork !== \\\"missing value\\\" && img.src !== song.onlineArtwork) {\\n img.crossOrigin = 'anonymous';\\n img.src = song.onlineArtwork;\\n } else {\\n dispatch({ type: \\\"DEFAULT_ART\\\" });\\n }\\n }\\n\\n img.crossOrigin = 'anonymous';\\n img.src = song.localArtwork;\\n}\\n\\n// RENDERING //\\n// Artwork image\\nconst ArtworkImage = ({ artwork: { art1, art2, alternate }, wrapperClass }) => (\\n \\n \\n \\n \\n)\\n\\n// Update notification component\\nconst UpdateNotif = ({ dispatch, updatePending, useBackground, color = 'inherit' }) => {\\n const UpdateLink = (\\n \\n {updatePending ? (\\n Updating! You might need to restart Ubersicht when finished... ({Math.floor(_updateTimer)})\\n ) : (\\n dispatch({ type: \\\"PREPARE_UPDATE\\\" })}>An update is available!\\n )}\\n \\n );\\n\\n return useBackground ? (\\n \\n \\n \\n ) : (\\n \\n )\\n}\\n\\n// Big player component\\nconst Big = ({ state, dispatch }) => {\\n const {\\n song: { track, artist, album, elapsed, duration },\\n secondaryColor,\\n tercaryColor,\\n artwork,\\n updateAvailable,\\n updatePending\\n } = state;\\n\\n return (\\n \\n \\n {updateAvailable && }\\n \\n \\n {track}\\n {artist}\\n {album}\\n \\n \\n )\\n}\\n\\n// Medium player component\\nconst Medium = ({ state, dispatch }) => {\\n const {\\n song: { track, artist, elapsed, duration },\\n secondaryColor,\\n tercaryColor,\\n artwork,\\n updateAvailable,\\n updatePending\\n } = state;\\n\\n return (\\n \\n \\n {updateAvailable && }\\n \\n \\n {track}\\n {artist}\\n \\n \\n )\\n}\\n\\n// Small player component\\nconst Small = ({ state, dispatch }) => {\\n const {\\n song: { track, artist, album, elapsed, duration },\\n secondaryColor,\\n tercaryColor,\\n artwork,\\n updateAvailable,\\n updatePending\\n } = state;\\n\\n return (\\n \\n \\n \\n {track}\\n {artist}\\n {album}\\n {updateAvailable && }\\n \\n \\n \\n )\\n}\\n\\n// Mini player component\\nconst Mini = ({ state, dispatch }) => {\\n const {\\n song: { track, artist, elapsed, duration },\\n primaryColor,\\n secondaryColor,\\n artwork,\\n updateAvailable,\\n updatePending\\n } = state;\\n\\n return (\\n \\n \\n {options.miniArtwork && }\\n \\n {track}\\n {artist}\\n \\n \\n \\n {updateAvailable && }\\n \\n )\\n}\\n\\n// Render function\\nexport const render = (state, dispatch) => {\\n const { size, horizontalPosition, verticalPosition, alwaysShow } = options;\\n const { app, playing, appleMusicError, songChange, primaryColor, secondaryColor, tercaryColor, artwork, song, updateAvailable, updatePending } = state;\\n\\n // Determine widget visability\\n const showWidget = playing || (alwaysShow === 1 && app) || alwaysShow === 2;\\n\\n // When song changes, prepare artwork\\n if (songChange) {\\n prepareArtwork(dispatch, song);\\n }\\n\\n // If Apple Music is playing online music, warn the user that it cannot get information for it\\n if (appleMusicError) {\\n return (\\n \\n \\n

Online Apple Music detected

\\n

Unfortunately, the widget is unable to access songs being streamed online through Apple Music.

\\n

To fix this, please download your music to your library and play it locally from there.

\\n \\n \\n )\\n }\\n\\n // Render\\n return (size === \\\"mini\\\") ? (\\n \\n \\n \\n ) : (\\n \\n {size === 'big' && }\\n {size === 'medium' && }\\n {size === 'small' && }\\n \\n )\\n};\"}" } } ] } ]