Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quantum-kittens
GitHub Repository: quantum-kittens/platypus
Path: blob/main/frontend/vue/components/MiniComposer/MiniComposer.vue
3376 views
<template>
  <section class="mini-composer">
    <div
      ref="configRef"
      class="mini-composer__config-container"
    >
      <slot />
    </div>
    <Carousel
      ref="carouselRef"
      class="mini-composer__exercise-text"
      :disable-arrows="true"
      @onSelectedChange="selectedExerciseChange"
    />
    <div class="mini-composer__gates">
      <h1 class="mini-composer__gates__title">
        Gates
      </h1>
      <GatesPool :available-gates="currentStepData.availableGates" />
    </div>
    <div class="mini-composer__circuit-section">
      <h1 class="mini-composer__circuit-section__title">
        Circuit
      </h1>
      <Circuit
        :circuit-state="currentStepData.circuitState"
        :auto-measure-gate="currentStepData.autoMeasureGate"
        :max-lines="maxLines"
        @onCircuitChanged="checkCurrentStepCompleteness"
      />
    </div>
    <ProbablityChart
      v-if="currentProbabilities.length > 0"
      class="mini-composer__probability-chart"
      :probabilities="currentProbabilities"
    />
    <div
      ref="lessonRef"
      class="mini-composer__lesson"
      :class="{ 'mini-composer__lesson__hidden': !currentStepData.isCompleted }"
    />
    <div class="mini-composer__footer">
      <AppCta
        v-if="areAllStepsCompleted()"
        class="mini-composer__footer__restart-button"
        label="Start over"
        @click="onRestartButton()"
      />
      <div
        ref="footerInfoRef"
        class="mini-composer__footer__info"
        :class="{ 'mini-composer__footer__info__hidden': !currentStepData.isCompleted }"
      />
      <AppCta
        v-if="currentStepData.isCompleted && !areAllStepsCompleted()"
        class="mini-composer__footer__next-button"
        label="Next"
        @click="onNextButton()"
      />
      <SolutionStateIndicator
        v-if="areAllStepsCompleted()"
        class="mini-composer__footer__solution-state"
        :state="correctSolution"
      />
    </div>
  </section>
</template>

<script lang="ts">
import { ref } from '@vue/reactivity'
import { Options, Vue, prop } from 'vue-class-component'
import draggable from 'vuedraggable'
import Carousel from '../Carousel/Carousel.vue'
import KetCircuitLine from '../Sketch/KetCircuitLine.vue'
import AppCta from '../common/AppCta.vue'
import SolutionStateIndicator, { SolutionState } from '../common/SolutionStateIndicator.vue'
import GatesPool from './GatesPool.vue'
import Circuit from './Circuit.vue'
import ProbablityChart from './ProbablityChart.vue'
import { ExerciseStep, ComposerGate, emptyExerciseStep, ProbabilityState } from './composerTypes'
import { GateName } from './gateUtils'

class Props {
  goal = prop<String>({ default: 'mini-composer-solved', required: true })
}

@Options({
  components: {
    Carousel,
    KetCircuitLine,
    Circuit,
    GatesPool,
    draggable,
    ProbablityChart,
    AppCta,
    SolutionStateIndicator
  }
})
export default class MiniComposer extends Vue.with(Props) {
  configRef = ref<HTMLDivElement | null>(null)
  get configDiv () { return (this.configRef as unknown as HTMLDivElement) }
  carouselRef = ref<Carousel | null>(null)
  get carousel () { return (this.carouselRef as unknown as Carousel) }
  footerInfoRef = ref<HTMLDivElement | null>(null)
  get footerInfoDiv () { return (this.footerInfoRef as unknown as HTMLDivElement) }
  lessonRef = ref<HTMLDivElement | null>(null)
  get lessonDiv () { return (this.lessonRef as unknown as HTMLDivElement) }

  correctSolution = SolutionState.CORRECT

  exerciseSteps: ExerciseStep[] = []

  currentStepIdx = 0
  currentStepData: ExerciseStep = emptyExerciseStep()

  get currentProbabilities () {
    if (!this.currentStepData.isCompleted) {
      return this.currentStepData.startProbabilities
    }
    return this.currentStepData.endProbabilities
  }

  get maxLines () {
    return this.currentStepData.circuitStateGoal.length
  }

  lastGateId = 0

  mounted () {
    const instructionsElements = Array.from<HTMLElement>(this.configDiv.querySelectorAll('.instructions'))
    const lessonsElements = Array.from<HTMLElement>(this.configDiv.querySelectorAll('.lesson'))
    const infoElements = Array.from<HTMLElement>(this.configDiv.querySelectorAll('.info'))
    const circuitElements = Array.from<HTMLElement>(this.configDiv.querySelectorAll('.circuit'))

    lessonsElements.forEach(element => this.lessonDiv.appendChild(element))
    infoElements.forEach(element => this.footerInfoDiv.appendChild(element))

    this.exerciseSteps = circuitElements.map<ExerciseStep>((stepConfigElement: HTMLElement) => {
      const autoMeasureGate = !!stepConfigElement.querySelector('.autoMeasureGate')

      const availableGatesElement = stepConfigElement.querySelector('.availableGates') as HTMLElement
      let availableGates: ComposerGate[] = []
      if (availableGatesElement && availableGatesElement.textContent) {
        availableGates = this.stringToGateNameArray(availableGatesElement.textContent)
      }

      const initialCircuitElement = Array.from<HTMLElement>(stepConfigElement.querySelectorAll('.initialCircuit .qubit'))
      const circuitState: ComposerGate[][] = this.htmlElementsToCircuit(initialCircuitElement)

      const goalCircuitElements = Array.from<HTMLElement>(stepConfigElement.querySelectorAll('.goalCircuit .qubit'))
      const circuitStateGoal: ComposerGate[][] = this.htmlElementsToCircuit(goalCircuitElements)

      const startProbabilitiesElements = stepConfigElement.querySelector('.startProbabilities')
      let startProbabilities: ProbabilityState[] = []
      if (startProbabilitiesElements && startProbabilitiesElements.textContent) {
        startProbabilities = this.stringToProbabilities(startProbabilitiesElements.textContent)
      }

      const endProbabilitiesElements = stepConfigElement.querySelector('.endProbabilities')
      let endProbabilities: ProbabilityState[] = []
      if (endProbabilitiesElements && endProbabilitiesElements.textContent) {
        endProbabilities = this.stringToProbabilities(endProbabilitiesElements.textContent)
      }

      return {
        autoMeasureGate,

        availableGates,
        circuitState,
        circuitStateGoal,

        startProbabilities,
        endProbabilities,
        isCompleted: false
      }
    })

    instructionsElements.forEach(element => this.carousel.elementsWrapper.appendChild(element))
    this.carousel.updateSlides()

    this.currentStepData = this.cloneCurrentStepData()
  }

  stringToProbabilities (text: string) : ProbabilityState[] {
    return text.split(',')
      .filter(text => text !== '')
      .map<ProbabilityState>((probabilityText: string) => {
        const tuple = probabilityText.split(':').map(text => text.trim())
        return { key: tuple[0], value: parseFloat(tuple[1]) }
      })
  }

  stringToGateNameArray (text: string) : ComposerGate[] {
    return text.split(' ')
      .filter(text => text !== '')
      .map<ComposerGate>((gateText: string) => {
        if (Object.values(GateName).some(gate => gateText.startsWith(gate))) {
          const parts = gateText.split('(')
          const gateName = parts[0]
          const rotation = parts[1]?.split(')')[0]
          return { name: gateName as GateName, id: this.lastGateId++, rotation }
        }
        return { name: GateName.UNKNOWN, id: this.lastGateId++ }
      })
  }

  htmlElementsToCircuit (elements: HTMLElement[]) : ComposerGate[][] {
    return elements.map((qubitLine: HTMLElement) => {
      if (qubitLine.textContent === null) {
        return []
      }
      return this.stringToGateNameArray(qubitLine.textContent)
    })
  }

  cloneCurrentStepData () : ExerciseStep {
    const currentStepReference = this.exerciseSteps[this.currentStepIdx]
    if (!currentStepReference) {
      return emptyExerciseStep()
    }

    const step: ExerciseStep = {
      autoMeasureGate: currentStepReference.autoMeasureGate,
      availableGates: [],
      circuitState: [[]],
      circuitStateGoal: currentStepReference.circuitStateGoal,
      startProbabilities: currentStepReference.startProbabilities,
      endProbabilities: currentStepReference.endProbabilities,
      isCompleted: currentStepReference.isCompleted
    }

    if (step.isCompleted) {
      step.availableGates = []
      step.circuitState = currentStepReference.circuitStateGoal.map(line => Array.from(line))
    } else {
      step.availableGates = Array.from(currentStepReference.availableGates)
      step.circuitState = currentStepReference.circuitState.map(line => Array.from(line))
    }
    return step
  }

  selectedExerciseChange (value :number) {
    this.lessonDiv.children[this.currentStepIdx]?.classList.remove('mini-composer__current-slide')
    this.lessonDiv.children[value]?.classList.add('mini-composer__current-slide')
    this.footerInfoDiv.children[this.currentStepIdx]?.classList.remove('mini-composer__current-slide')
    this.footerInfoDiv.children[value]?.classList.add('mini-composer__current-slide')
    this.currentStepIdx = value
    this.currentStepData = this.cloneCurrentStepData()
  }

  checkCurrentStepCompleteness () {
    const currentCircuitState = this.currentStepData.circuitState
    const currentCircuitGoal = this.currentStepData.circuitStateGoal
    const hasSameLinesCount = currentCircuitState.length === currentCircuitGoal.length
    if (!hasSameLinesCount) {
      this.currentStepData.isCompleted = false
      return
    }

    const isCircuitCorrect = currentCircuitState.every((qubitLineState, lineIdx) => {
      const qubitLineGoal = currentCircuitGoal[lineIdx]
      const hasSameGatesCount = qubitLineState.length === qubitLineGoal.length

      const isLineCorrect = hasSameGatesCount && qubitLineState.every((gate, gateIdx) => qubitLineGoal[gateIdx] && this.areSameGate(gate, qubitLineGoal[gateIdx]))
      return isLineCorrect
    })

    if (!isCircuitCorrect) {
      this.currentStepData.isCompleted = false
      return
    }

    this.exerciseSteps[this.currentStepIdx].isCompleted = true
    this.currentStepData = this.cloneCurrentStepData()
  }

  areSameGate (gateA: ComposerGate, gateB: ComposerGate) {
    return gateA.name === gateB.name &&
           gateA.rotation === gateB.rotation
  }

  areAllStepsCompleted () {
    const isExerciseCompleted = this.exerciseSteps.every(step => step.isCompleted)
    if (isExerciseCompleted) {
      this.$step?.score(this.goal as string)
    }

    return isExerciseCompleted
  }

  onNextButton () {
    this.carousel.nextSlide()
  }

  onRestartButton () {
    this.exerciseSteps.forEach((step) => { step.isCompleted = false })
    this.carousel.nextSlide()
  }
}
</script>
<style scoped lang="scss">
@import 'carbon-components/scss/globals/scss/typography';
@import 'carbon-components/scss/globals/scss/layout';
@import '../../../scss/variables/colors.scss';
@import '~/../scss/variables/mq.scss';

.mini-composer {
  display: grid;
  min-height: 36rem;
  grid-template-columns: 320px 1fr 160px;
  grid-template-rows: min-content min-content min-content auto min-content;
  grid-template-areas:
    "text text text"
    "gates lesson lesson"
    "circuit lesson lesson"
    "chart lesson lesson"
    "footer footer footer";

  &__config-container {
    display: none;
  }

  &__exercise-text {
    grid-area: text;
    border-bottom: 1px solid $border-color;

    ::v-deep(.carousel__selector) {
      margin-top: 0;
    }
  }
  &__gates {
    grid-area: gates;
    border-bottom: 1px solid $border-color;
    padding: 0 $spacing-05 $spacing-05 $spacing-05;
    &__title {
      margin: $spacing-03 0 $spacing-05 0;
      @include type-style('heading-01');
    }
  }
  &__circuit-section {
    grid-area: circuit;
    padding: 0 $spacing-04;
    &__title {
      margin-top: $spacing-03;
      @include type-style('heading-01');
    }
    &__qubit-line {
      position: relative;
      &__slot-container {
        display: flex;
        flex-flow: row;

        position: absolute;
        top: 0px;
        height: 100%;
        width: 100%;
        align-items: center;
        padding: 0 $spacing-07;
      }
      &__slot {
        display: flex;
        flex-flow: row;
        flex: 1 1 auto;
        max-width: 100%;

        &__gate {
          margin-right: $spacing-02;
          &.sortable-ghost {
            transition: opacity 0.2s ease-out;
            opacity: 0.5;
          }
        }
      }
      &__z-gate {
        flex: 0 0 auto;
      }
    }
  }
  &__probability-chart {
    grid-area: chart;
    height: 250px;
    padding: $spacing-07 $spacing-09 $spacing-07 $spacing-05;
  }
  &__lesson {
    grid-area: lesson;
    border-left: 1px solid $border-color;
    padding: $spacing-06;
    margin-bottom: $spacing-10;

    > :not(.mini-composer__current-slide) {
      display: none;
    }

    &__hidden > ::v-deep(.mini-composer__current-slide) {
      display: none;
    }
  }
  &__footer {
    grid-area: footer;
    display: flex;
    justify-content: flex-end;
    min-height: 52px;

    &__restart-button {
      flex: 0 0 auto;
      width: 10rem;
      cursor: pointer;
      ::v-deep(.app-cta__icon) {
        transform: rotate(180deg);
      }
    }
    &__info {
      @include type-style('body-long-01');
      flex: 1;
      display: flex;
      align-items: center;
      min-height: 52px;
      padding: 0 $spacing-05;

      background-color: $background-color-light;

      &__hidden {
        display: none;
      }

      > :not(.mini-composer__current-slide) {
        display: none;
      }
    }
    &__next-button {
      flex: 0 0 auto;
      width: 10rem;
      cursor: pointer;
    }
    &__solution-state {
      flex: 0 0 auto;
      width: 11rem;
    }
  }
}
</style>